diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index ae17ee40c..000000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -**/zz_gen_*.* linguist-generated -docs/data/zz_cli_help.toml linguist-generated diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index ea3fd9a3a..eb48f29c7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,4 +1,4 @@ -name: 🐞 Bug Report +name: Bug Report description: Create a report to help us improve. labels: [bug] body: @@ -7,9 +7,9 @@ body: attributes: label: Welcome options: - - label: Yes, I'm using a binary release within the two latest releases. + - label: Yes, I'm using a binary release within 2 latest releases. required: true - - label: Yes, I've searched for similar issues on GitHub and didn't find any. + - label: Yes, I've searched similar issues on GitHub and didn't find any. required: true - label: Yes, I've included all information below (version, config, etc). required: true @@ -35,7 +35,6 @@ body: attributes: label: How do you use lego? options: - - I don't know - Library - Binary - Docker image @@ -43,10 +42,6 @@ body: - Through Caddy - Through Terraform ACME provider - Through Bitnami - - Through 1Panel - - Through Zoraxy - - Through Certimate - - go install - Other validations: required: true @@ -67,9 +62,8 @@ body: - type: textarea id: version attributes: - label: Effective version of lego + label: Version of lego description: |- - `latest` or `dev` are not effective versions. ```console $ lego --version ``` diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 0d51e6a8c..8db3a8333 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: false contact_links: - - name: ❓ Questions + - name: Questions url: https://github.com/go-acme/lego/discussions about: If you have a question, or are looking for advice, please post on our Discussions section! - - name: 📖 Documentation + - name: lego documentation url: https://go-acme.github.io/lego/ about: Please take a look to our documentation. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 7f6793167..efc118cc0 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,4 +1,4 @@ -name: 💡 Feature request +name: Feature request description: Suggest an idea for this project. body: - type: checkboxes @@ -6,7 +6,7 @@ body: attributes: label: Welcome options: - - label: Yes, I've searched for similar issues on GitHub and didn't find any. + - label: Yes, I've searched similar issues on GitHub and didn't find any. required: true - type: dropdown @@ -14,7 +14,6 @@ body: attributes: label: How do you use lego? options: - - I don't know - Library - Binary - Docker image @@ -22,22 +21,10 @@ body: - Through Caddy - Through Terraform ACME provider - Through Bitnami - - Through 1Panel - - Through Zoraxy - - Through Certimate - - go install - Other validations: required: true - - type: input - id: version - attributes: - label: Effective version of lego - description: "`latest` or `dev` are not effective versions." - validations: - required: true - - type: textarea id: description attributes: diff --git a/.github/ISSUE_TEMPLATE/new_dns_provider.yml b/.github/ISSUE_TEMPLATE/new_dns_provider.yml index b319bc287..ae3d0e352 100644 --- a/.github/ISSUE_TEMPLATE/new_dns_provider.yml +++ b/.github/ISSUE_TEMPLATE/new_dns_provider.yml @@ -1,4 +1,4 @@ -name: 🧩 New DNS provider support +name: New DNS provider support description: Request for the support of a new DNS provider. title: "Support for provider: " labels: [enhancement, new-provider] @@ -8,21 +8,15 @@ body: attributes: label: Welcome options: - - label: Yes, I've searched for similar issues on GitHub and didn't find any. + - label: Yes, I've searched similar issues on GitHub and didn't find any. required: true - label: Yes, the DNS provider exposes a public API. required: true - label: Yes, I know that the lego maintainers don't have an account in all DNS providers in the world. required: true - - - type: checkboxes - id: pr - attributes: - label: Implementation - options: - label: Yes, I'm able to create a pull request and be able to maintain the implementation. required: false - - label: Yes, I can test an implementation with the help of the maintainers if someone creates a pull request. + - label: Yes, I'm able to test an implementation if someone creates a pull request to add the support of this DNS provider. required: false - type: dropdown @@ -30,7 +24,6 @@ body: attributes: label: How do you use lego? options: - - I don't know - Library - Binary - Docker image @@ -38,25 +31,10 @@ body: - Through Caddy - Through Terraform ACME provider - Through Bitnami - - Through 1Panel - - Through Zoraxy - - Through Certimate - - go install - Other validations: required: true - - type: dropdown - id: profile - attributes: - label: Who are you? - options: - - A customer of this DNS provider - - An employee of this DNS provider - - Other (please explain) - validations: - required: true - - type: input id: provider-link attributes: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 795320a8d..000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,12 +0,0 @@ - diff --git a/.github/PULL_REQUEST_TEMPLATE/mnp.md b/.github/PULL_REQUEST_TEMPLATE/mnp.md deleted file mode 100644 index e0b09890c..000000000 --- a/.github/PULL_REQUEST_TEMPLATE/mnp.md +++ /dev/null @@ -1,30 +0,0 @@ -PULL REQUEST TEMPLATE FOR MAINTAINERS ONLY. - -https://github.com/go-acme/lego/compare/master...ldez:branch?quick_pull=1&title=Add+DNS+provider+for+&labels=enhancement,area/dnsprovider,state/need-user-tests&template=mnp.md - -?quick_pull=1&title=Add+DNS+provider+for+&labels=enhancement,area/dnsprovider,state/need-user-tests&template=mnp.md - ---- - -- [x] adds a description to your PR -- [x] have a homogeneous design with the other providers -- [ ] add tests (units) -- [ ] add tests ("live") -- [ ] add a provider descriptor -- [ ] generate CLI help, documentation, and readme. -- [ ] be able to do: _(and put the output of this command to a comment)_ - ```bash - make build - rm -rf .lego - - EXAMPLE_USERNAME=xxx \ - ./dist/lego -m your_email@example.com --dns EXAMPLE -d *.example.com -d example.com -s https://acme-staging-v02.api.letsencrypt.org/directory run - ``` - Note the wildcard domain is important. -- [ ] pass the linter -- [ ] do `go mod tidy` - -Ping @xxx, can you run the command (with your domain, email, credentials, etc.)? - -Closes # - diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 4f9d444fc..26637201a 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -12,16 +12,20 @@ jobs: runs-on: ubuntu-latest env: GO_VERSION: stable - HUGO_VERSION: 0.148.2 + HUGO_VERSION: 0.117.0 CGO_ENABLED: 0 steps: - - uses: actions/checkout@v6 + # https://github.com/marketplace/actions/checkout + - name: Check out code + uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-go@v6 + # https://github.com/marketplace/actions/setup-go-environment + - name: Set up Go ${{ env.GO_VERSION }} + uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} diff --git a/.github/workflows/go-cross.yml b/.github/workflows/go-cross.yml index 9dee85035..bcfdc62aa 100644 --- a/.github/workflows/go-cross.yml +++ b/.github/workflows/go-cross.yml @@ -16,12 +16,17 @@ jobs: strategy: matrix: - go-version: [ oldstable, stable ] + go-version: [ stable ] os: [ubuntu-latest, macos-latest, windows-latest] steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + # https://github.com/marketplace/actions/checkout + - name: Checkout code + uses: actions/checkout@v4 + + # https://github.com/marketplace/actions/setup-go-environment + - name: Set up Go ${{ matrix.go-version }} + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 33ca106cc..038ed624b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -13,44 +13,54 @@ jobs: runs-on: ubuntu-latest env: GO_VERSION: stable - GOLANGCI_LINT_VERSION: v2.10 - HUGO_VERSION: 0.148.2 + GOLANGCI_LINT_VERSION: v1.59.0 + HUGO_VERSION: 0.117.0 CGO_ENABLED: 0 LEGO_E2E_TESTS: CI MEMCACHED_HOSTS: localhost:11211 steps: - - uses: actions/checkout@v6 + # https://github.com/marketplace/actions/checkout + - name: Check out code + uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-go@v6 + # https://github.com/marketplace/actions/setup-go-environment + - name: Set up Go ${{ env.GO_VERSION }} + uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} - name: Check and get dependencies run: | - go mod tidy --diff + go mod tidy + git diff --exit-code go.mod + git diff --exit-code go.sum - - name: Generate and Check generated elements + # https://golangci-lint.run/usage/install#other-ci + - name: Install golangci-lint ${{ env.GOLANGCI_LINT_VERSION }} run: | - make generate-dns - git diff --exit-code - - - uses: golangci/golangci-lint-action@v9 - with: - version: ${{ env.GOLANGCI_LINT_VERSION }} - install-only: true + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCI_LINT_VERSION} + golangci-lint --version - name: Install Pebble - run: go install github.com/letsencrypt/pebble/v2/cmd/pebble@v2.9.0 + run: go install github.com/letsencrypt/pebble/v2/cmd/pebble@3fe019bbc0a41ed16e2fee31592bb91751acaa47 - name: Install challtestsrv - run: go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@v2.9.0 + run: go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@3fe019bbc0a41ed16e2fee31592bb91751acaa47 - name: Set up a Memcached server - run: docker run -d --rm -p 11211:11211 memcached:1.6-alpine + uses: niden/actions-memcached@v7 + + - name: Setup /etc/hosts + run: | + echo "127.0.0.1 acme.wtf" | sudo tee -a /etc/hosts + echo "127.0.0.1 lego.wtf" | sudo tee -a /etc/hosts + echo "127.0.0.1 acme.lego.wtf" | sudo tee -a /etc/hosts + echo "127.0.0.1 légô.wtf" | sudo tee -a /etc/hosts + echo "127.0.0.1 xn--lg-bja9b.wtf" | sudo tee -a /etc/hosts - name: Make run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6a0d3b703..8c22b7ad9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,11 +5,6 @@ on: tags: - v* -permissions: - # Allow the workflow to write attestations. - id-token: write - attestations: write - jobs: release: @@ -42,11 +37,13 @@ jobs: docker-images: true swap-storage: false - - uses: actions/checkout@v6 + - name: Check out code + uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-go@v6 + - name: Set up Go ${{ env.GO_VERSION }} + uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} @@ -59,6 +56,11 @@ jobs: - name: Install snapcraft run: sudo snap install snapcraft --classic + - name: Snapcraft login + env: + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + run: snapcraft login + - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -67,21 +69,9 @@ jobs: # https://goreleaser.com/ci/actions/ - name: Run GoReleaser - id: goreleaser - uses: goreleaser/goreleaser-action@v6 + uses: goreleaser/goreleaser-action@v5 with: - version: v2.13.0 + version: latest args: release -p 1 --clean --timeout=90m env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN_REPO }} - SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} - AUR_KEY: ${{ secrets.AUR_KEY }} - - - uses: actions/attest-build-provenance@v3 - with: - subject-checksums: ./dist/lego_${{ fromJSON(steps.goreleaser.outputs.metadata).version }}_checksums.txt - github-token: ${{ secrets.GH_TOKEN_REPO }} - - uses: actions/attest-build-provenance@v3 - with: - subject-checksums: ./dist/digests.txt - github-token: ${{ secrets.GH_TOKEN_REPO }} diff --git a/.golangci.yml b/.golangci.yml index b6ab51ccc..d62ad647f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,284 +1,238 @@ -version: "2" +run: + timeout: 10m -formatters: - enable: - - gci - - gofmt - - gofumpt - - goimports - settings: - gofumpt: - extra-rules: true - gofmt: - rewrite-rules: - - pattern: 'interface{}' - replacement: 'any' +linters-settings: + govet: + enable: + - shadow + gocyclo: + min-complexity: 12 + goconst: + min-len: 3 + min-occurrences: 3 + funlen: + lines: -1 + statements: 50 + misspell: + locale: US + ignore-words: + - internetbs + depguard: + rules: + main: + deny: + - pkg: "github.com/instana/testify" + desc: not allowed + - pkg: "github.com/pkg/errors" + desc: Should be replaced by standard lib errors package + tagalign: + align: false + order: + - xml + - json + - yaml + - yml + - toml + - mapstructure + - url + godox: + keywords: + - FIXME + gocritic: + enabled-tags: + - diagnostic + - style + - performance + disabled-checks: + - paramTypeCombine # already handle by gofumpt.extra-rules + - whyNoLint # already handle by nonolint + - unnamedResult + - hugeParam + - sloppyReassign + - rangeValCopy + - octalLiteral + - ptrToRefParam + - appendAssign + - ruleguard + - httpNoBody + - exposedSyncMutex + revive: + rules: + - name: struct-tag + - name: blank-imports + - name: context-as-argument + - name: context-keys-type + - name: dot-imports + - name: error-return + - name: error-strings + - name: error-naming + - name: exported + disabled: true + - name: if-return + - name: increment-decrement + - name: var-naming + - name: var-declaration + - name: package-comments + disabled: true + - name: range + - name: receiver-naming + - name: time-naming + - name: unexported-return + - name: indent-error-flow + - name: errorf + - name: empty-block + - name: superfluous-else + - name: unused-parameter + disabled: true + - name: unreachable-code + - name: redefines-builtin-id + testifylint: + disable: + - require-error + - go-require + perfsprint: + err-error: true + errorf: true + sprintf1: true + strconcat: false linters: - default: all + enable-all: true disable: - - wsl # Deprecated - - bodyclose - - canonicalheader - - contextcheck + - gomnd # deprecated - cyclop # duplicate of gocyclo - - dupl # not relevant - - err113 # not relevant - - errchkjson - - errname - - exhaustive # not relevant - - exhaustruct # not relevant - - forbidigo - - forcetypeassert - - gosec - - gosmopolitan # not relevant - - ireturn # not relevant - - lll - - makezero # not relevant - - mnd - - musttag # false-positive https://github.com/junk1tm/musttag/issues/17 - - nestif # too many false-positive - - nilnil # not relevant - - nlreturn # not relevant - - noctx - - noinlineerr # too strict - - nonamedreturns - - paralleltest # not relevant - - prealloc # too many false-positive - - rowserrcheck # not relevant (SQL) - sqlclosecheck # not relevant (SQL) - - tagliatelle + - rowserrcheck # not relevant (SQL) + - execinquery # not relevant (SQL) + - lll + - gosec + - dupl # not relevant + - prealloc # too many false-positive + - bodyclose # too many false-positive + - mnd - testpackage # not relevant - tparallel # not relevant - - varnamelen # not relevant + - paralleltest # not relevant + - nestif # too many false-positive - wrapcheck - - settings: - depguard: - rules: - main: - deny: - - pkg: github.com/instana/testify - desc: not allowed - - pkg: github.com/pkg/errors - desc: Should be replaced by standard lib errors package - funlen: - lines: -1 - statements: 50 - goconst: - min-len: 3 - min-occurrences: 3 - gocritic: - disabled-checks: - - paramTypeCombine # already handle by gofumpt.extra-rules - - whyNoLint # already handle by nonolint - - unnamedResult - - hugeParam - - sloppyReassign - - rangeValCopy - - octalLiteral - - ptrToRefParam - - appendAssign - - ruleguard - - httpNoBody - - exposedSyncMutex - enabled-tags: - - diagnostic - - style - - performance - gocyclo: - min-complexity: 12 - godox: - keywords: - - FIXME - govet: - disable: - - fieldalignment - enable-all: true - settings: - printf: - funcs: - - Print - - Printf - - Warn - - Warnf - - Fatal - - Fatalf - misspell: - locale: US - ignore-rules: - - internetbs - perfsprint: - err-error: true - errorf: true - sprintf1: true - strconcat: false - revive: - rules: - - name: struct-tag - - name: blank-imports - - name: context-as-argument - - name: context-keys-type - - name: dot-imports - - name: error-return - - name: error-strings - - name: error-naming - - name: exported - disabled: true - - name: if-return - - name: increment-decrement - - name: var-naming - - name: var-declaration - - name: package-comments - disabled: true - - name: range - - name: receiver-naming - - name: time-naming - - name: unexported-return - - name: indent-error-flow - - name: errorf - - name: empty-block - - name: superfluous-else - - name: unused-parameter - disabled: true - - name: unreachable-code - - name: redefines-builtin-id - tagalign: - align: false - order: - - xml - - json - - yaml - - yml - - toml - - mapstructure - - url - testifylint: - disable: - - require-error - - go-require - usetesting: - os-setenv: false # we already have a test "framework" to handle env vars - funcorder: - struct-method: false - - exclusions: - warn-unused: true - presets: - - comments - - std-error-handling - paths: - # Those elements are related to code borrowed from the official HuaweiCloud API client. - - providers/dns/huaweicloud/internal - rules: - - path: (.+)_test.go - linters: - - funlen - - goconst - - maintidx - - path: (.+)_test.go - text: Error return value of `fmt.Fprintln` is not checked - linters: - - errcheck - - text: "var-naming: avoid meaningless package names" - linters: - - revive - - text: "var-naming: avoid package names that conflict with Go standard library package names" - linters: - - revive - - path: certcrypto/crypto.go - text: (tlsFeatureExtensionOID|ocspMustStapleFeature) is a global variable - linters: - - gochecknoglobals - - path: challenge/dns01/nameserver.go - text: (defaultNameservers|recursiveNameservers|fqdnSoaCache|muFqdnSoaCache) is a global variable - linters: - - gochecknoglobals - - path: challenge/dns01/nameserver_.+.go - text: dnsTimeout is a global variable - linters: - - gochecknoglobals - - path: challenge/dns01/precheck.go - text: defaultNameserverPort is a global variable - linters: - - gochecknoglobals - - path: challenge/http01/domain_matcher.go - text: cyclomatic complexity \d+ of func `parseForwardedHeader` is high - linters: - - gocyclo - - path: challenge/http01/domain_matcher.go - text: Function 'parseForwardedHeader' has too many statements - linters: - - funlen - - path: challenge/tlsalpn01/tls_alpn_challenge.go - text: idPeAcmeIdentifierV1 is a global variable - linters: - - gochecknoglobals - - path: log/logger.go - text: Logger is a global variable - linters: - - gochecknoglobals - - path: e2e/(dnschallenge/)?[\d\w]+_test.go - text: load is a global variable - linters: - - gochecknoglobals - - path: providers/(dns|http)/([\d\w]+/)*[\d\w]+_test.go - text: envTest is a global variable - linters: - - gochecknoglobals - - path: providers/dns/namecheap/namecheap_test.go - text: testCases is a global variable - linters: - - gochecknoglobals - - path: providers/dns/namecheap/transport.go - text: (envProxyOnce|envProxyFuncValue) is a global variable - linters: - - gochecknoglobals - - path: providers/dns/acmedns/mock_test.go - text: egTestAccount is a global variable - linters: - - gochecknoglobals - - path: providers/http/memcached/memcached_test.go - text: memcachedHosts is a global variable - linters: - - gochecknoglobals - - path: providers/dns/checkdomain/internal/types.go - text: '`payed` is a misspelling of `paid`' - linters: - - misspell - - path: platform/tester/env_test.go - linters: - - thelper - - path: providers/dns/oraclecloud/oraclecloud_test.go - text: 'SA1019: x509.EncryptPEMBlock has been deprecated since Go 1.16' - linters: - - staticcheck - - path: providers/dns/sakuracloud/wrapper.go - text: mu is a global variable - linters: - - gochecknoglobals - - path: cmd/cmd_renew.go - text: cyclomatic complexity \d+ of func `(renewForDomains|renewForCSR)` is high - linters: - - gocyclo - - path: cmd/cmd_renew.go - text: Function 'renewForDomains' has too many statements - linters: - - funlen - - path: providers/dns/cpanel/cpanel.go - text: cyclomatic complexity 13 of func `\(\*DNSProvider\)\.CleanUp` is high - linters: - - gocyclo - - path: providers/dns/manual/manual.go - text: 'SA1019: dns01.DNSProviderManual is deprecated' - linters: - - staticcheck - # Those elements have been replaced by non-exposed structures. - - path: providers/dns/linode/linode_test.go - text: 'SA1019: linodego\.(DomainsPagedResponse|DomainRecordsPagedResponse) is deprecated' - linters: - - staticcheck + - err113 # not relevant + - nlreturn # not relevant + - wsl # not relevant + - exhaustive # not relevant + - exhaustruct # not relevant + - makezero # not relevant + - forbidigo + - varnamelen # not relevant + - nilnil # not relevant + - ireturn # not relevant + - contextcheck # too many false-positive + - tenv # we already have a test "framework" to handle env vars + - noctx + - forcetypeassert + - tagliatelle + - errname + - errchkjson + - nonamedreturns + - musttag # false-positive https://github.com/junk1tm/musttag/issues/17 + - gosmopolitan # not relevant + - exportloopref # Useless with go1.22 + - canonicalheader # Can create side effects in the context of API clients issues: + exclude-use-default: false max-issues-per-linter: 0 max-same-issues: 0 + exclude: + - 'Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked' + - 'exported (type|method|function) (.+) should have comment or be unexported' + - 'ST1000: at least one file in a package should have a package comment' + exclude-rules: + - path: (.+)_test.go + linters: + - funlen + - goconst + - maintidx + - path: (.+)_test.go + text: 'Error return value of `fmt.Fprintln` is not checked' + linters: + - errcheck + - path: providers/dns/dns_providers.go + linters: + - gocyclo + - path: providers/dns/gcloud/googlecloud_test.go + text: 'string `(lego\.wtf|manhattan)` has (\d+) occurrences, make it a constant' + - path: providers/dns/zoneee/zoneee_test.go + text: 'string `(bar|foo)` has (\d+) occurrences, make it a constant' + - path: certcrypto/crypto.go + text: '(tlsFeatureExtensionOID|ocspMustStapleFeature) is a global variable' + - path: challenge/dns01/nameserver.go + text: '(defaultNameservers|recursiveNameservers|fqdnSoaCache|muFqdnSoaCache) is a global variable' + - path: challenge/dns01/nameserver_.+.go + text: 'dnsTimeout is a global variable' + - path: challenge/dns01/nameserver_test.go + text: 'findXByFqdnTestCases is a global variable' + - path: challenge/http01/domain_matcher.go + text: 'string `Host` has \d occurrences, make it a constant' + - path: challenge/http01/domain_matcher.go + text: 'cyclomatic complexity \d+ of func `parseForwardedHeader` is high' + - path: challenge/http01/domain_matcher.go + text: "Function 'parseForwardedHeader' has too many statements" + - path: challenge/tlsalpn01/tls_alpn_challenge.go + text: 'idPeAcmeIdentifierV1 is a global variable' + - path: log/logger.go + text: 'Logger is a global variable' + - path: 'e2e/(dnschallenge/)?[\d\w]+_test.go' + text: load is a global variable + - path: 'providers/dns/([\d\w]+/)*[\d\w]+_test.go' + text: 'envTest is a global variable' + - path: 'providers/http/([\d\w]+/)*[\d\w]+_test.go' + text: 'envTest is a global variable' + - path: providers/dns/namecheap/namecheap_test.go + text: 'testCases is a global variable' + - path: providers/dns/acmedns/acmedns_test.go + text: 'egTestAccount is a global variable' + - path: providers/http/memcached/memcached_test.go + text: 'memcachedHosts is a global variable' + - path: providers/dns/sakuracloud/client_test.go + text: 'cyclomatic complexity 13 of func `(TestDNSProvider_cleanupTXTRecord_concurrent|TestDNSProvider_addTXTRecord_concurrent)` is high' + - path: providers/dns/dns_providers.go + text: "Function 'NewDNSChallengeProviderByName' has too many statements" + - path: cmd/flags.go + text: "Function 'CreateFlags' is too long" + - path: certificate/certificates.go + text: "Function 'GetOCSP' is too long" + - path: providers/dns/otc/client.go + text: "Function 'loginRequest' is too long" + - path: providers/dns/gandi/gandi.go + text: "Function 'Present' is too long" + - path: cmd/zz_gen_cmd_dnshelp.go + linters: + - gocyclo + - funlen + - path: providers/dns/checkdomain/internal/types.go + text: '`payed` is a misspelling of `paid`' + - path: providers/dns/namecheap/namecheap_test.go + text: 'cognitive complexity (\d+) of func `TestDNSProvider_getHosts` is high' + - path: platform/tester/env_test.go + linters: + - thelper + - path: providers/dns/oraclecloud/oraclecloud_test.go + text: 'SA1019: x509.EncryptPEMBlock has been deprecated since Go 1.16' + - path: challenge/http01/domain_matcher.go + text: 'yodaStyleExpr' + - path: providers/dns/dns_providers.go + text: 'Function name: NewDNSChallengeProviderByName,' + - path: providers/dns/sakuracloud/wrapper.go + text: 'mu is a global variable' + - path: providers/dns/hosttech/internal/client_test.go + text: 'Duplicate words \(0\) found' + - path: cmd/cmd_renew.go + text: 'cyclomatic complexity \d+ of func `(renewForDomains|renewForCSR)` is high' + - path: providers/dns/cpanel/cpanel.go + text: 'cyclomatic complexity 13 of func `\(\*DNSProvider\)\.CleanUp` is high' + +output: + sort-results: true + sort-order: + - linter + - file diff --git a/.goreleaser.yml b/.goreleaser.yml index c358f8a38..59674a287 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,11 +1,9 @@ -version: 2 - project_name: lego builds: - binary: lego - main: ./cmd/lego/ + main: ./cmd/lego/main.go env: - CGO_ENABLED=0 flags: @@ -14,9 +12,9 @@ builds: - -s -w -X main.version={{.Version}} goos: - - linux - - darwin - windows + - darwin + - linux - freebsd - openbsd - solaris @@ -42,10 +40,6 @@ builds: goarch: 386 - goos: openbsd goarch: arm - # Deprecated in go1.25, Removed in go1.26 - # https://go.dev/doc/go1.25#windows - - goos: windows - goarch: arm changelog: sort: asc @@ -55,98 +49,123 @@ changelog: - '(?i)^Detach v[\d|.]+' - '(?i)^Prepare release v[\d|.]+' -release: - skip_upload: false - github: - owner: 'go-acme' - name: 'lego' - header: | - lego is an independent, free, and open-source project, if you value it, consider [supporting it](https://donate.ldez.dev)! ❤️ - - Everybody thinks that the others will donate, but in the end, nobody does. - - So if you think that lego is worth it, please consider [donating](https://donate.ldez.dev). - - For key updates, see the [changelog](https://github.com/go-acme/lego/blob/HEAD/CHANGELOG.md#v{{ .Major }}{{ .Minor }}{{ .Patch }}). - archives: - id: lego name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}' - formats: ['tar.gz'] + format: tar.gz format_overrides: - goos: windows - formats: ['zip'] + format: zip files: - LICENSE - CHANGELOG.md -dockers_v2: - - images: - - 'goacme/lego' +docker_manifests: + - name_template: 'goacme/lego:{{ .Tag }}' + image_templates: + - 'goacme/lego:{{ .Tag }}-amd64' + - 'goacme/lego:{{ .Tag }}-arm64' + - 'goacme/lego:{{ .Tag }}-armv7' + - name_template: 'goacme/lego:latest' + image_templates: + - 'goacme/lego:{{ .Tag }}-amd64' + - 'goacme/lego:{{ .Tag }}-arm64' + - 'goacme/lego:{{ .Tag }}-armv7' + - name_template: 'goacme/lego:v{{ .Major }}.{{ .Minor }}' + image_templates: + - 'goacme/lego:v{{ .Major }}.{{ .Minor }}-amd64' + - 'goacme/lego:v{{ .Major }}.{{ .Minor }}-arm64' + - 'goacme/lego:v{{ .Major }}.{{ .Minor }}-armv7' + +dockers: + - use: buildx + goos: linux + goarch: amd64 dockerfile: buildx.Dockerfile - platforms: - - linux/amd64 - - linux/arm64 - - linux/arm/v7 - tags: - - 'latest' - - 'v{{ .Major }}' - - 'v{{ .Major }}.{{ .Minor }}' - - '{{ .Tag }}' - labels: + image_templates: + - 'goacme/lego:latest-amd64' + - 'goacme/lego:{{ .Tag }}-amd64' + - 'goacme/lego:v{{ .Major }}.{{ .Minor }}-amd64' + build_flag_templates: + - '--pull' # https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys - 'org.opencontainers.image.title': '{{.ProjectName}}' - 'org.opencontainers.image.description': 'Lets Encrypt/ACME client and library written in Go' - 'org.opencontainers.image.source': '{{.GitURL}}' - 'org.opencontainers.image.url': '{{.GitURL}}' - 'org.opencontainers.image.documentation': 'https://go-acme.github.io/lego' - 'org.opencontainers.image.created': '{{.Date}}' - 'org.opencontainers.image.revision': '{{.FullCommit}}' - 'org.opencontainers.image.version': '{{.Version}}' + - '--label=org.opencontainers.image.title={{.ProjectName}}' + - '--label=org.opencontainers.image.description=Lets Encrypt/ACME client and library written in Go' + - '--label=org.opencontainers.image.source={{.GitURL}}' + - '--label=org.opencontainers.image.url={{.GitURL}}' + - '--label=org.opencontainers.image.documentation=https://go-acme.github.io/lego' + - '--label=org.opencontainers.image.created={{.Date}}' + - '--label=org.opencontainers.image.revision={{.FullCommit}}' + - '--label=org.opencontainers.image.version={{.Version}}' + - '--platform=linux/amd64' + + - use: buildx + goos: linux + goarch: arm64 + dockerfile: buildx.Dockerfile + image_templates: + - 'goacme/lego:latest-arm64' + - 'goacme/lego:{{ .Tag }}-arm64' + - 'goacme/lego:v{{ .Major }}.{{ .Minor }}-arm64' + build_flag_templates: + - '--pull' + # https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys + - '--label=org.opencontainers.image.title={{.ProjectName}}' + - '--label=org.opencontainers.image.description=Lets Encrypt/ACME client and library written in Go' + - '--label=org.opencontainers.image.source={{.GitURL}}' + - '--label=org.opencontainers.image.url={{.GitURL}}' + - '--label=org.opencontainers.image.documentation=https://go-acme.github.io/lego' + - '--label=org.opencontainers.image.created={{.Date}}' + - '--label=org.opencontainers.image.revision={{.FullCommit}}' + - '--label=org.opencontainers.image.version={{.Version}}' + - '--platform=linux/arm64' + + - use: buildx + goos: linux + goarch: arm + goarm: '7' + dockerfile: buildx.Dockerfile + image_templates: + - 'goacme/lego:latest-armv7' + - 'goacme/lego:{{ .Tag }}-armv7' + - 'goacme/lego:v{{ .Major }}.{{ .Minor }}-armv7' + build_flag_templates: + - '--pull' + # https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys + - '--label=org.opencontainers.image.title={{.ProjectName}}' + - '--label=org.opencontainers.image.description=Lets Encrypt/ACME client and library written in Go' + - '--label=org.opencontainers.image.source={{.GitURL}}' + - '--label=org.opencontainers.image.url={{.GitURL}}' + - '--label=org.opencontainers.image.documentation=https://go-acme.github.io/lego' + - '--label=org.opencontainers.image.created={{.Date}}' + - '--label=org.opencontainers.image.revision={{.FullCommit}}' + - '--label=org.opencontainers.image.version={{.Version}}' + - '--platform=linux/arm/v7' snapcrafts: - - name_template: "{{ .ProjectName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" - disable: false - publish: true + - name: lego grade: stable confinement: strict license: MIT base: core22 + publish: true summary: Lego is a Let's Encrypt/ACME client. description: | Lego is a Let's Encrypt/ACME client written in Go. - + The lego snap makes it easy to install and use Lego on any Linux distribution that supports snaps. - + Usage: * `sudo snap install lego` * `sudo lego --email="you@example.com" --domains="example.com" --server=https://acme-staging-v02.api.letsencrypt.org/directory --http --http.port :8080 run + + channel_templates: + - edge + apps: lego: - command: lego + command: bin/lego environment: LEGO_PATH: /var/snap/lego/common/.lego plugs: - network-bind - -aurs: - - description: "Let s Encrypt client and ACME library written in Go" - skip_upload: false - homepage: https://go-acme.github.io/lego/ - name: 'lego-bin' - provides: - - lego - maintainers: - - "Fernandez Ludovic " - license: APACHE - private_key: "{{ .Env.AUR_KEY }}" - git_url: "ssh://aur@aur.archlinux.org/lego-bin.git" - commit_author: - name: ldez - email: ldez@users.noreply.github.com - package: |- - # Bin - install -Dm755 "./lego" "${pkgdir}/usr/bin/lego" - - # License - install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/lego/LICENSE" diff --git a/CHANGELOG.md b/CHANGELOG.md index ae73f70f3..7c1b3e1cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,528 +1,6 @@ # Changelog -lego is an independent, free, open-source project, if you value it, consider [supporting it](https://donate.ldez.dev)! ❤️ - -Everybody thinks that the others will donate, but in the end, nobody does. - -So if you think that lego is worth it, please consider [donating](https://donate.ldez.dev). - -## v4.32.0 - -- Release date: 2026-02-19 -- Tag: [v4.32.0](https://github.com/go-acme/lego/releases/tag/v4.32.0) - -### Added - -- **[dnsprovider]** Add DNS provider for ArtFiles -- **[dnsprovider]** Add DNS provider for Leaseweb -- **[dnsprovider]** Add DNS provider for FusionLayer NameSurfer -- **[dnsprovider]** Add DNS provider for DDNSS -- **[dnsprovider]** Add DNS provider for Bluecat v2 -- **[dnsprovider]** Add DNS provider for TodayNIC/时代互联 -- **[dnsprovider]** Add DNS provider for DNSExit -- **[dnsprovider]** alidns: add line record option - -### Changed - -- **[dnsprovider]** azure: reinforces deprecation -- **[dnsprovider]** allinkl: detect zone through API - -### Fixed - -- **[ari]** fix: implement parsing for Retry-After header according to RFC 7231 -- **[dnsprovider]** namesurfer: fix updateDNSHost -- **[dnsprovider]** timewebcloud: fix subdomain support -- **[dnsprovider]** fix: deduplicate authz for DNS01 challenge -- **[lib,cli]** fix: use IPs to define the main domain -- **[lib]** fix: preserve domain order - -## v4.31.0 - -- Release date: 2026-01-08 -- Tag: [v4.31.0](https://github.com/go-acme/lego/releases/tag/v4.31.0) - -### Added - -- **[dnsprovider]** Add DNS provider for ISPConfig -- **[dnsprovider]** Add DNS Provider for ISPConfig (DDNS Module) -- **[dnsprovider]** Add DNS provider for Alwaysdata -- **[dnsprovider]** Add DNS provider for JDCloud -- **[dnsprovider]** Add DNS provider for 35.com/三五互联 -- **[dnsprovider]** f5xc: add an option to configure the domain of the server - -### Changed - -- **[lib]** feat: improve ACME error types -- **[dnsprovider,cname]** namedotcom: follow CNAME - -### Fixed - -- **[dnsprovider]** hetzner: fix compatibility with _FILE suffix -- **[dnsprovider]** gandiv5: fix API Key header - -## v4.30.1 - -- Release date: 2025-12-16 -- Tag: [v4.30.1](https://github.com/go-acme/lego/releases/tag/v4.30.1) - -Due to an error related to `aliyun/credentials-go`, some artifacts of the v4.30.0 release have not been published. - -This release contains the same things as v4.30.0. - -## v4.30.0 - -- Release date: 2025-12-16 -- Tag: [v4.30.0](https://github.com/go-acme/lego/releases/tag/v4.30.0) - -### Added - -- **[dnsprovider]** Add DNS provider for Ionos Cloud -- **[dnsprovider]** Add DNS provider for Virtualname -- **[dnsprovider]** Add DNS Provider for Neodigit -- **[dnsprovider]** Add DNS provider for Syse.no -- **[dnsprovider]** Add DNS provider for Gravity -- **[dnsprovider]** Add DNS provider for hosting.nl - -### Changed - -- **[cli]** feat: remove email requirement - -### Fixed - -- **[dnsprovider]** autodns: use the right response structure - -## v4.29.0 - -- Release date: 2025-11-29 -- Tag: [v4.29.0](https://github.com/go-acme/lego/releases/tag/v4.29.0) - -### Added - -- **[dnsprovider]** Add DNS provider for United-Domains -- **[dnsprovider]** Add DNS provider for Gigahost.no -- **[dnsprovider]** Add DNS provider for EdgeCenter -- **[dnsprovider]** Add DNS provider for AlibabaCloud ESA -- **[dnsprovider]** edgeone: add zones mapping -- **[dnsprovider]** namecheap: add experimental proxy support - -### Changed - -- **[dnsprovider]** gandiv5: update base API URL - -### Fixed - -- **[dnsprovider]** hetzner: use int64 for IDs -- **[dnsprovider]** baiducloud: pagination and TTL -- **[dnsprovider]** inwx: fix API breaking changes with record IDs - -## v4.28.1 - -- Release date: 2025-11-06 -- Tag: [v4.28.1](https://github.com/go-acme/lego/releases/tag/v4.28.1) - -### Fixed - -- **[cli]** fix: skip nil response - -## v4.28.0 - -- Release date: 2025-10-31 -- Tag: [v4.28.0](https://github.com/go-acme/lego/releases/tag/v4.28.0) - -### Added - -- **[dnsprovider]** Add DNS provider for Anexia -- **[dnsprovider]** Add DNS provider for webnames.ca -- **[dnsprovider]** webnames: rename to webnamesru to avoid ambiguity with webnamesca - -### Changed - -- **[dnsprovider,log]** hetzner: add deprecation logs -- **[dnsprovider]** iwantmyname: provider deprecation -- **[cli]** improve retryable HTTP client error handling - -### Fixed - -- **[dnsprovider]** hostinger: fix record update - -## v4.27.0 - -- Release date: 2025-10-17 -- Tag: [v4.27.0](https://github.com/go-acme/lego/releases/tag/v4.27.0) - -### Added - -- **[dnsprovider]** Add DNS provider for Octenium -- **[dnsprovider]** Add DNS provider for Hostinger -- **[dnsprovider]** Add DNS provider for Beget.com - -### Changed - -- **[cli]** support `--private-key` with a PKCS#8 keypair -- **[dnsprovider]** hetzner: update to new API -- **[dnsprovider]** otc: adds option to use private zone - -### Fixed - -- **[lib]** fix: deduplicate order identifiers - -## v4.26.0 - -- Release date: 2025-09-13 -- Tag: [v4.26.0](https://github.com/go-acme/lego/releases/tag/v4.26.0) - -### Added - -- **[dnsprovider]** Add DNS provider for KeyHelp -- **[dnsprovider]** Add DNS provider for Binary Lane -- **[dnsprovider]** Add DNS provider for Tencent EdgeOne -- **[dnsprovider]** azuredns: pipeline credential support -- **[dnsprovider]** oraclecloud: handle instance_principal authentication - -### Changed - -- **[dnsprovider]** oraclecloud: add env var aliases -- **[dnsprovider]** simply: update to API v2 -- **[lib,cli]** EAB: fallback to base64.URLEncoding - -### Fixed - -- **[dnsprovider]** selectelv2: add missing options - -## v4.25.2 - -- Release date: 2025-08-06 -- Tag: [v4.25.2](https://github.com/go-acme/lego/releases/tag/v4.25.2) - -### Changed - -- **[cli,log]** log when dynamic renew date not yet reached - -### Fixed - -- **[cli]** fix: remove wrong env var -- **[lib,cli]** fix: enforce HTTPS to the ACME server - -## v4.25.1 - -- Release date: 2025-07-21 -- Tag: [v4.25.1](https://github.com/go-acme/lego/releases/tag/v4.25.1) - -### Fixed - -- **[cli]** fix: wrong CLI flag type - -## v4.25.0 - -- Release date: 2025-07-21 -- Tag: [v4.25.0](https://github.com/go-acme/lego/releases/tag/v4.25.0) - -The binary size of this release is about ~50% smaller compared to previous releases. - -This will also reduce the module cache usage by 320 MB (this will only affect users of lego as a library or who build lego themselves). - -### Added - -- **[dnsprovider]** Add DNS provider for ZoneEdit -- **[cli]** Add an option to define dynamically the renew date -- **[lib,cli]** Add an option to disable common name in CSR - -### Changed - -- **[dnsprovider]** vinyldns: add an option to add quotes around the TXT record value -- **[dnsprovider]** ionos: increase default propagation timeout - -### Fixed - -- **[cli]** fix: enforce domain into renewal command - -## v4.24.0 - -- Release date: 2025-07-07 -- Tag: [v4.24.0](https://github.com/go-acme/lego/releases/tag/v4.24.0) - -### Added - -- **[dnsprovider]** Add DNS provider for Azion -- **[dnsprovider]** Add DNS provider for DynDnsFree.de -- **[dnsprovider]** Add DNS provider for ConoHa v3 -- **[dnsprovider]** Add DNS provider for RU Center -- **[dnsprovider]** gcloud: add service account impersonation - -### Changed - -- **[dnsprovider]** pdns: improve error messages -- **[dnsprovider]** cloudflare: add quotation marks to TXT record -- **[dnsprovider]** googledomains: provider deprecation -- **[dnsprovider]** mijnhost: improve record filter - -### Fixed - -- **[dnsprovider]** exoscale: fix find record -- **[dnsprovider]** nicmanager: fix mode env var name and value -- **[lib,cli]** Check order identifiers difference between client and server - -## v4.23.1 - -- Release date: 2025-04-16 -- Tag: [v4.23.1](https://github.com/go-acme/lego/releases/tag/v4.23.1) - -Due to an error related to Snapcraft, some artifacts of the v4.23.0 release have not been published. - -This release contains the same things as v4.23.0. - -## v4.23.0 - -- Release date: 2025-04-16 -- Tag: [v4.23.0](https://github.com/go-acme/lego/releases/tag/v4.23.0) - -### Added - -- **[dnsprovider]** Add DNS provider for Active24 -- **[dnsprovider]** Add DNS provider for BookMyName -- **[dnsprovider]** Add DNS provider for Axelname -- **[dnsprovider]** Add DNS provider for Baidu Cloud -- **[dnsprovider]** Add DNS provider for Metaregistrar -- **[dnsprovider]** Add DNS provider for F5 XC -- **[dnsprovider]** Add INFOBLOX_CA_CERTIFICATE option -- **[dnsprovider]** route53: adds option to use private zone -- **[dnsprovider]** edgedns: add account switch key option -- **[dnsprovider]** infoblox: update API client to v2 -- **[lib,cli]** Add delay option for TLSALPN challenge - -### Changed - -- **[dnsprovider]** designate: speed up API requests by using filters -- **[dnsprovider]** cloudflare: make base URL configurable -- **[dnsprovider]** websupport: migrate to API v2 -- **[dnsprovider]** dnssimple: use GetZone - -### Fixed - -- **[ari]** Fix retry on `alreadyReplaced` error -- **[cli,log]** Fix malformed log messages -- **[cli]** Kill hook when the command is stuck -- **[dnsprovider]** pdns: fix TXT record cleanup for wildcard domains -- **[dnsprovider]** allinkl: remove `ReturnInfo` - -## v4.22.2 - -- Release date: 2025-02-17 -- Tag: [v4.22.2](https://github.com/go-acme/lego/releases/tag/v4.22.2) - -### Fixed - -- **[dnsprovider]** acme-dns: use new registred account - -## v4.22.1 - -- Release date: 2025-02-17 -- Tag: [v4.22.1](https://github.com/go-acme/lego/releases/tag/v4.22.1) - -### Fixed - -- **[dnsprovider]** acme-dns: continue the process when the CNAME is handled by the storage - -### Added - -## v4.22.0 - -- Release date: 2025-02-17 -- Tag: [v4.22.0](https://github.com/go-acme/lego/releases/tag/v4.22.0) - -### Added - -- **[cli]** Add `--private-key` flag to set the private key. -- **[cli]** Add `LEGO_DEBUG_ACME_HTTP_CLIENT` environment variable to debug the calls to the ACME server. -- **[cli]** Add `LEGO_EMAIL` environment variable for specifying email. -- **[cli]** Add `--hook-timeout` flag to run and renew commands. -- **[dnsprovider]** Add DNS provider for myaddr.{tools,dev,io} -- **[dnsprovider]** Add DNS provider for Spaceship -- **[dnsprovider]** acme-dns: add HTTP storage -- **[lib,cli,httpprovider]** Add `--http.delay` option for HTTP challenge. -- **[lib,cli,profiles]** Add support for Profiles Extension. -- **[lib]** Add an option to set CSR email addresses - -### Changed - -- **[lib]** rewrite status management -- **[dnsprovider]** docs: improve units and default values - -### Removed - -- **[dnsprovider]** netcup: remove TTL option - -### Fixed - -- **[cli,log]** remove extra debug logs - -## v4.21.0 - -- Release date: 2024-12-20 -- Tag: [v4.21.0](https://github.com/go-acme/lego/releases/tag/v4.21.0) - -### Added - -- **[dnsprovider]** Add DNS provider for Rainyun/雨云 -- **[dnsprovider]** Add DNS provider for West.cn/西部数码 -- **[dnsprovider]** Add DNS provider for ManageEngine CloudDNS -- **[cli]** feat: add --force-cert-domains flag to renew - -### Fixed - -- **[cli]** create client only when needed -- **[cli]** clone the transport with tls-skip-verify -- **[cli]** use retryable client for ACME server calls -- **[dnsprovider]** bunny: fix zone detection -- **[dnsprovider]** inwx: delete only the TXT record related to the DNS challenge -- **[dnsprovider]** infomaniak: increase default propagation timeout -- **[dnsprovider]** dnsmadeeasy: use default transport -- **[dnsprovider]** netcup: increase default propagation values -- **[dnsprovider]** otc: use default transport - -## v4.20.4 - -- Release date: 2024-11-21 -- Tag: [v4.20.4](https://github.com/go-acme/lego/releases/tag/v4.20.4) - -Publish the Snap to the Snapcraft stable channel. - -## v4.20.3 - -- Release date: 2024-11-21 -- Tag: [v4.20.3](https://github.com/go-acme/lego/releases/tag/v4.20.3) - -### Fixed - -- **[dnsprovider]** technitium: fix status code handling -- **[dnsprovider]** directadmin: fix timeout configuration -- **[httpprovider]** fix: HTTP server IPv6 matching - -## v4.20.2 - -- Release date: 2024-11-11 -- Tag: [v4.20.2](https://github.com/go-acme/lego/releases/tag/v4.20.2) - -### Added - -- **[dnsprovider]** Add DNS provider for Technitium -- **[dnsprovider]** Add DNS provider for Regfish -- **[dnsprovider]** Add DNS provider for Timeweb Cloud -- **[dnsprovider]** Add DNS provider for Volcano Engine -- **[dnsprovider]** Add DNS provider for Core-Networks -- **[dnsprovider]** rfc2136: add support for tsig-keygen generated file -- **[cli]** Add option to skip the TLS verification of the ACME server -- Add documentation for env var only options - -### Changed - -- **[cli,ari]** Attempt to check ARI unless explicitly disabled -- **[dnsprovider]** Improve propagation check error messages -- **[dnsprovider]** cloudxns: provider deprecation -- **[dnsprovider]** brandit: provider deprecation - -### Fixed - -- **[dnsprovider]** regru: update authentication method -- **[dnsprovider]** selectelv2: fix non-ASCII domain -- **[dnsprovider]** limacity: fix error message -- **[dnsprovider]** volcengine: set API information within the default configuration -- **[log]** Parse printf verbs in log line output - -## v4.20.1 - -- Release date: 2024-11-11 - -Cancelled due to CI failure. - -## v4.20.0 - -- Release date: 2024-11-11 - -Cancelled due to CI failure. - -## v4.19.2 - -- Release date: 2024-10-06 -- Tag: [v4.19.2](https://github.com/go-acme/lego/releases/tag/v4.19.2) - -### Fixed - -- **[lib]** go1.22 compatibility - -## v4.19.1 - -- Release date: 2024-10-06 -- Tag: [v4.19.1](https://github.com/go-acme/lego/releases/tag/v4.19.1) - -### Fixed - -- **[dnsprovider]** selectelv2: use baseURL from configuration -- **[dnsprovider]** epik: add User-Agent - -## v4.19.0 - -- Release date: 2024-10-03 -- Tag: [v4.19.0](https://github.com/go-acme/lego/releases/tag/v4.19.0) - -### Added - -- **[dnsprovider]** Add DNS provider for HuaweiCloud -- **[dnsprovider]** Add DNS provider for SelfHost.(de|eu) -- **[lib,cli,dnsprovider]** Add `dns.propagation-rns` option -- **[cli,dnsprovider]** Add `dns.propagation-wait` flag -- **[lib,dnsprovider]** Add `PropagationWait` function - -### Changed - -- **[dnsprovider]** ionos: follow CNAME -- **[lib,dnsprovider]** Reducing the lock strength of the soa cache entry -- **[lib,cli,dnsprovider]** Deprecation of `dns.disable-cp`, replaced by `dns.propagation-disable-ans`. - -### Fixed - -- **[dnsprovider]** Use UTC instead of GMT when possible -- **[dnsprovider]** namesilo: restrict CleanUp -- **[dnsprovider]** godaddy: fix cleanup - -## v4.18.0 - -- Release date: 2024-08-30 -- Tag: [v4.18.0](https://github.com/go-acme/lego/releases/tag/v4.18.0) - -### Added - -- **[dnsprovider]** Add DNS provider for mijn.host -- **[dnsprovider]** Add DNS provider for Lima-City -- **[dnsprovider]** Add DNS provider for DirectAdmin -- **[dnsprovider]** Add DNS provider for Mittwald -- **[lib,cli]** feat: add option to handle the overall request limit -- **[lib]** feat: expose certificates pool creation - -### Changed - -- **[cli]** feat: add LEGO_ISSUER_CERT_PATH to run hook -- **[dnsprovider]** bluecat: skip deploy -- **[dnsprovider]** ovh: allow to use ovh.conf file -- **[dnsprovider]** designate: allow manually overwriting DNS zone - -### Fixed - -- **[ari]** fix: avoid Int63n panic in ShouldRenewAt() - -## v4.17.4 - -- Release date: 2024-06-12 -- Tag: [v4.17.4](https://github.com/go-acme/lego/releases/tag/v4.17.4) - -### Fixed - -- **[dnsprovider]** Update dependencies - -## v4.17.3 - -- Release date: 2024-05-28 -- Tag: [v4.17.3](https://github.com/go-acme/lego/releases/tag/v4.17.3) +## [v4.17.0] - 2024-05-28 ### Added @@ -550,44 +28,13 @@ Cancelled due to CI failure. - **[dnsprovider]** pdns: reconstruct zone URLs to enable non-root folder API endpoints - **[dnsprovider]** alidns: fix link to API documentation -## v4.17.2 - -- Release date: 2024-05-28 - -Canceled due to a release failure related to Snapcraft. - -The Snapcraft release are disabled for now. - -## v4.17.1 - -- Release date: 2024-05-28 - -Canceled due to a release failure related to oci-go-sdk. - -The module `github.com/oracle/oci-go-sdk/v65` uses `github.com/gofrs/flock` but flock doesn't support some platform (like Solaris): -- https://github.com/gofrs/flock/issues/60 - -Due to that we will remove the Solaris build. - -## v4.17.0 - -- Release date: 2024-05-28 - -Canceled due to a release failure related to Snapcraft. - -## v4.16.1 - -- Release date: 2024-03-10 -- Tag: [v4.16.1](https://github.com/go-acme/lego/releases/tag/v4.16.1) +## [v4.16.1] - 2024-03-10 ### Fixed - **[cli,ari]** fix: don't generate ARI cert ID if ARI is not enable -## v4.16.0 - -- Release date: 2024-03-09 -- Tag: [v4.16.0](https://github.com/go-acme/lego/releases/tag/v4.16.0) +## [v4.16.0] - 2024-03-09 ### Added @@ -608,10 +55,7 @@ Canceled due to a release failure related to Snapcraft. - **[dnsprovider]** easydns: fix zone detection - **[dnsprovider]** ns1: fix record creation -## v4.15.0 - -- Release date: 2024-01-28 -- Tag: [v4.15.0](https://github.com/go-acme/lego/releases/tag/v4.15.0) +## [v4.15.0] - 2024-01-28 ### Added @@ -649,10 +93,7 @@ Canceled due to a release failure related to Snapcraft. - **[dnsprovider]** nifcloud: fix API requests - **[dnsprovider]** otc: sequential challenge -## v4.14.1 - -- Release date: 2023-09-20 -- Tag: [v4.14.1](https://github.com/go-acme/lego/releases/tag/v4.14.1) +## [v4.14.1] - 2023-09-20 ### Fixed @@ -660,16 +101,11 @@ Canceled due to a release failure related to Snapcraft. - **[dnsprovider]** bunny: use NRDCG fork - **[dnsprovider]** ovh: update client to v1.4.2 -## v4.14.1 - -- Release date: 2023-09-19 +## [v4.14.1] - 2023-09-19 Cancelled due to CI failure. -## v4.14.0 - -- Release date: 2023-08-20 -- Tag: [v4.14.0](https://github.com/go-acme/lego/releases/tag/v4.14.0) +## [v4.14.0] - 2023-08-20 ### Added @@ -688,29 +124,20 @@ Cancelled due to CI failure. - **[dnsprovider]** pdns: fix notify - **[dnsprovider]** route53: avoid unexpected records deletion -## v4.13.3 - -- Release date: 2023-07-25 -- Tag: [v4.13.3](https://github.com/go-acme/lego/releases/tag/v4.13.3) +## [v4.13.3] - 2023-07-25 ### Fixed - **[dnsprovider]** azuredns: fix configuration from env vars - **[dnsprovider]** gcore: change API domain -## v4.13.2 - -- Release date: 2023-07-21 -- Tag: [v4.13.2](https://github.com/go-acme/lego/releases/tag/v4.13.2) +## [v4.13.2] - 2023-07-21 ### Fixed - **[dnsprovider]** servercow: fix regression -## v4.13.1 - -- Release date: 2023-07-20 -- Tag: [v4.13.1](https://github.com/go-acme/lego/releases/tag/v4.13.1) +## [v4.13.1] - 2023-07-20 ### Added @@ -731,35 +158,24 @@ Cancelled due to CI failure. - **[cli]** fix: list command - **[lib]** fix: ARI explanationURL -## v4.13.0 - -- Release date: 2023-07-20 +## [v4.13.0] - 2023-07-20 Cancelled due to a CI issue (no space left on device). -## v4.12.2 - -- Release date: 2023-06-19 -- Tag: [v4.12.2](https://github.com/go-acme/lego/releases/tag/v4.12.2) +## [v4.12.2] - 2023-06-19 ### Fixed - **[dnsprovider]** dnsmadeeasy: fix DeleteRecord - **[lib]** fix: read status code from response -## v4.12.1 - -- Release date: 2023-06-06 -- Tag: [v4.12.1](https://github.com/go-acme/lego/releases/tag/v4.12.1) +## [v4.12.1] - 2023-06-06 ### Fixed - **[dnsprovider]** pdns: fix record value -## v4.12.0 - -- Release date: 2023-05-28 -- Tag: [v4.12.0](https://github.com/go-acme/lego/releases/tag/v4.12.0) +## [v4.12.0] - 2023-05-28 ### Added @@ -777,10 +193,7 @@ Cancelled due to a CI issue (no space left on device). - **[dnsprovider]** autodns: fixes wrong zone in api call if CNAME is used - **[cli]** fix: archive only domain-related files on revoke -## v4.11.0 - -- Release date: 2023-05-02 -- Tag: [v4.11.0](https://github.com/go-acme/lego/releases/tag/v4.11.0) +## [v4.11.0] - 2023-05-02 ### Added @@ -802,27 +215,18 @@ Cancelled due to a CI issue (no space left on device). - **[dnsprovider]** rimuhosting: fix API base URL -## v4.10.2 - -- Release date: 2023-02-26 -- Tag: [v4.10.2](https://github.com/go-acme/lego/releases/tag/v4.10.2) +## [v4.10.2] - 2023-02-26 Fix Docker image builds. -## v4.10.1 - -- Release date: 2023-02-25 -- Tag: [v4.10.1](https://github.com/go-acme/lego/releases/tag/v4.10.1) +## [v4.10.1] - 2023-02-25 ### Fixed - **[dnsprovider,cname]** acmedns: fix CNAME support - **[dnsprovider]** dynu: fix subdomain support -## v4.10.0 - -- Release date: 2023-02-10 -- Tag: [v4.10.0](https://github.com/go-acme/lego/releases/tag/v4.10.0) +## [v4.10.0] - 2023-02-10 ### Added @@ -848,28 +252,22 @@ Fix Docker image builds. - **[dnsprovider]** pdns: fix usage of notify only when zone kind is Master or Slave - **[dnsprovider]** return an error when extracting record name -## v4.9.1 - -- Release date: 2022-11-25 -- Tag: [v4.9.1](https://github.com/go-acme/lego/releases/tag/v4.9.1) +## [v4.9.1] - 2022-11-25 ### Changed - +- - **[lib,cname]** cname: add log about CNAME entries - **[dnsprovider]** regru: improve error handling ### Fixed - +- - **[dnsprovider,cname]** fix CNAME support for multiple DNS providers - **[dnsprovider,cname]** duckdns: fix CNAME support - **[dnsprovider,cname]** oraclecloud: use fqdn to resolve zone - **[dnsprovider]** hurricane: fix CNAME support - **[lib,cname]** cname: stop trying to traverse cname if none have been found -## v4.9.0 - -- Release date: 2022-10-03 -- Tag: [v4.9.0](https://github.com/go-acme/lego/releases/tag/v4.9.0) +## [v4.9.0] - 2022-10-03 ### Added @@ -899,10 +297,7 @@ Fix Docker image builds. - **[dnsprovider]** njalla: fix record id unmarshal error - **[dnsprovider]** tencentcloud: fix subdomain error -## v4.8.0 - -- Release date: 2022-06-30 -- Tag: [v4.8.0](https://github.com/go-acme/lego/releases/tag/v4.8.0) +## [v4.8.0] - 2022-06-30 ### Added @@ -918,12 +313,9 @@ Fix Docker image builds. - **[dnsprovider]** hetzner: set min TTL to 60s - **[docs]** refactoring and cleanup -## v4.7.0 +## [v4.7.0] - 2022-05-27 -- Release date: 2022-05-27 -- Tag: [v4.7.0](https://github.com/go-acme/lego/releases/tag/v4.7.0) - -### Added +### Added: - **[dnsprovider]** Add DNS provider for iwantmyname - **[dnsprovider]** Add DNS Provider for IIJ DNS Platform Service @@ -932,21 +324,18 @@ Fix Docker image builds. - **[dnsprovider]** dnsimple: add debug option - **[cli]** feat: add `LEGO_CERT_PEM_PATH` and `LEGO_CERT_PFX_PATH` -### Changed +### Changed: - **[dnsprovider]** gcore: change dns api url - **[dnsprovider]** bluecat: rewrite provider implementation -### Fixed +### Fixed: - **[dnsprovider]** rfc2136: fix TSIG secret - **[dnsprovider]** tencentcloud: fix InvalidParameter.DomainInvalid error when using DNS challenges - **[lib]** fix: panic in certcrypto.ParsePEMPrivateKey -## v4.6.0 - -- Release date: 2022-01-18 -- Tag: [v4.6.0](https://github.com/go-acme/lego/releases/tag/v4.6.0) +## [v4.6.0] - 2022-01-18 ### Added @@ -968,21 +357,15 @@ Fix Docker image builds. - **[dnsprovider]** mythicbeasts: fix token expiration - **[dnsprovider]** rackspace: change zone ID to string -## v4.5.3 +## [v4.5.3] - 2021-09-06 -- Release date: 2021-09-06 -- Tag: [v4.5.3](https://github.com/go-acme/lego/releases/tag/v4.5.3) - -### Fixed +### Fixed: - **[lib,cli]** fix: missing preferred chain param for renew request -## v4.5.2 +## [v4.5.2] - 2021-09-01 -- Release date: 2021-09-01 -- Tag: [v4.5.2](https://github.com/go-acme/lego/releases/tag/v4.5.2) - -### Added +### Added: - **[dnsprovider]** Add DNS provider for all-inkl - **[dnsprovider]** Add DNS provider for Epik @@ -993,7 +376,7 @@ Fix Docker image builds. - **[dnsprovider]** Add DNS provider for Internet.bs - **[dnsprovider]** Add DNS provider for nicmanager -### Changed +### Changed: - **[dnsprovider]** alidns: support ECS instance RAM role - **[dnsprovider]** alidns: support sts token credential @@ -1001,7 +384,7 @@ Fix Docker image builds. - **[dnsprovider]** ovh: follow cname - **[lib,cli]** Add AlwaysDeactivateAuthorizations flag to ObtainRequest -### Fixed +### Fixed: - **[dnsprovider]** infomaniak: fix subzone support - **[dnsprovider]** edgedns: fix Present and CleanUp logic @@ -1010,24 +393,17 @@ Fix Docker image builds. - **[lib]** lib: use permanent error instead of context cancellation - **[dnsprovider]** desec: bump to v0.6.0 -## v4.5.1 - -- Release date: 2021-10-01 +## [v4.5.1] - 2021-09-01 Cancelled due to a CI issue, replaced by v4.5.2. -## v4.5.0 - -- Release date: 2021-09-30 +## [v4.5.0] - 2021-09-30 Cancelled due to a CI issue, replaced by v4.5.2. -## v4.4.0 +## [v4.4.0] - 2021-06-08 -- Release date: 2021-06-08 -- Tag: [v4.4.0](https://github.com/go-acme/lego/releases/tag/v4.4.0) - -### Added +### Added: - **[dnsprovider]** Add DNS provider for Infoblox - **[dnsprovider]** Add DNS provider for Porkbun @@ -1036,7 +412,7 @@ Cancelled due to a CI issue, replaced by v4.5.2. - **[dnsprovider]** Add DNS provider for VinylDNS - **[dnsprovider]** Add DNS provider for wedos -### Changed +### Changed: - **[cli]** log: Use stderr instead of stdout. - **[dnsprovider]** hostingde: autodetection of the zone name. @@ -1044,7 +420,7 @@ Cancelled due to a CI issue, replaced by v4.5.2. - **[dnsprovider]** powerdns: several improvements - **[lib]** lib: improve wait.For returns. -### Fixed +### Fixed: - **[dnsprovider]** hurricane: add API rate limiter. - **[dnsprovider]** hurricane: only treat first word of response body as response code @@ -1053,21 +429,15 @@ Cancelled due to a CI issue, replaced by v4.5.2. - **[dnsprovider]** nifcloud: Get zone info from dns01.FindZoneByFqdn - **[cli,lib]** csr: Support the type `NEW CERTIFICATE REQUEST` -## v4.3.1 +## [v4.3.1] - 2021-03-12 -- Release date: 2021-03-12 -- Tag: [v4.3.1](https://github.com/go-acme/lego/releases/tag/v4.3.1) - -### Fixed +### Fixed: - **[dnsprovider]** exoscale: fix dependency version. -## v4.3.0 +## [v4.3.0] - 2021-03-10 -- Release date: 2021-03-10 -- Tag: [v4.3.0](https://github.com/go-acme/lego/releases/tag/v4.3.0) - -### Added +### Added: - **[dnsprovider]** Add DNS provider for Njalla - **[dnsprovider]** Add DNS provider for Domeneshop @@ -1075,13 +445,13 @@ Cancelled due to a CI issue, replaced by v4.5.2. - **[dnsprovider]** designate: support for Openstack Application Credentials - **[dnsprovider]** edgedns: support for .edgerc file -### Changed +### Changed: - **[dnsprovider]** infomaniak: Make error message more meaningful - **[dnsprovider]** cloudns: Improve reliability - **[dnsprovider]** rfc2163: Removed support for MD5 algorithm. The default algorithm is now SHA1. -### Fixed +### Fixed: - **[dnsprovider]** desec: fix error with default TTL - **[dnsprovider]** mythicbeasts: implement `ProviderTimeout` @@ -1089,146 +459,119 @@ Cancelled due to a CI issue, replaced by v4.5.2. - **[lib]** Increase HTTP client timeouts - **[lib]** preferred chain only match root name -## v4.2.0 +## [v4.2.0] - 2021-01-24 -- Release date: 2021-01-24 -- Tag: [v4.2.0](https://github.com/go-acme/lego/releases/tag/v4.2.0) - -### Added +### Added: - **[dnsprovider]** Add DNS provider for Loopia - **[dnsprovider]** Add DNS provider for Ionos. -### Changed +### Changed: - **[dnsprovider]** acme-dns: update cpu/goacmedns to v0.1.1. - **[dnsprovider]** inwx: Increase propagation timeout to 360s to improve robustness - **[dnsprovider]** vultr: Update to govultr v2 API - **[dnsprovider]** pdns: get exact zone instead of all zones -### Fixed +### Fixed: - **[dnsprovider]** vult, dnspod: fix default HTTP timeout. - **[dnsprovider]** pdns: URL request creation. - **[lib]** errors: Fix instance not being printed -## v4.1.3 +## [v4.1.3] - 2020-11-25 -- Release date: 2020-11-25 -- Tag: [v4.1.3](https://github.com/go-acme/lego/releases/tag/v4.1.3) - -### Fixed +### Fixed: - **[dnsprovider]** azure: fix error handling. -## v4.1.2 +## [v4.1.2] - 2020-11-21 -- Release date: 2020-11-21 -- Tag: [v4.1.2](https://github.com/go-acme/lego/releases/tag/v4.1.2) - -### Fixed +### Fixed: - **[lib]** fix: preferred chain support. -## v4.1.1 +## [v4.1.1] - 2020-11-19 -- Release date: 2020-11-19 -- Tag: [v4.1.1](https://github.com/go-acme/lego/releases/tag/v4.1.1) - -### Fixed +### Fixed: - **[dnsprovider]** otc: select correct zone if multiple returned - **[dnsprovider]** azure: fix target must be a non-nil pointer -## v4.1.0 +## [v4.1.0] - 2020-11-06 -- Release date: 2020-11-06 -- Tag: [v4.1.0](https://github.com/go-acme/lego/releases/tag/v4.1.0) - -### Added +### Added: - **[dnsprovider]** Add DNS provider for Infomaniak - **[dnsprovider]** joker: add support for SVC API - **[dnsprovider]** gcloud: add an option to allow the use of private zones -### Changed +### Changed: - **[dnsprovider]** rfc2136: ensure TSIG algorithm is fully qualified - **[dnsprovider]** designate: Deprecate OS_TENANT_NAME as required field -### Fixed +### Fixed: - **[lib]** acme/api: use postAsGet instead of post for AccountService.Get - **[lib]** fix: use http.Header.Set method instead of Add. -## v4.0.1 +## [v4.0.1] - 2020-09-03 -- Release date: 2020-09-03 -- Tag: [v4.0.1](https://github.com/go-acme/lego/releases/tag/v4.0.1) - -### Fixed +### Fixed: - **[dnsprovider]** exoscale: change dependency version. -## v4.0.0 +## [v4.0.0] - 2020-09-02 -- Release date: 2020-09-02 -- Tag: [v4.0.0](https://github.com/go-acme/lego/releases/tag/v4.0.0) - -### Added +### Added: - **[cli], [lib]** Support "alternate" certificate links for selecting different signing Chains -### Changed +### Changed: - **[cli]** Replaces `ec384` by `ec256` as default key-type - **[lib]** Changes `ObtainForCSR` method signature -### Removed +### Removed: - **[dnsprovider]** Replaces FastDNS by EdgeDNS - **[dnsprovider]** Removes old Linode provider - **[lib]** Removes `AddPreCheck` function -## v3.9.0 +## [v3.9.0] - 2020-09-01 -- Release date: 2020-09-01 -- Tag: [v3.9.0](https://github.com/go-acme/lego/releases/tag/v3.9.0) - -### Added +### Added: - **[dnsprovider]** Add Akamai Edgedns. Deprecate FastDNS - **[dnsprovider]** Add DNS provider for HyperOne -### Changed +### Changed: - **[dnsprovider]** designate: add support for Openstack clouds.yaml - **[dnsprovider]** azure: allow selecting environments - **[dnsprovider]** desec: applies API rate limits. -### Fixed +### Fixed: - **[dnsprovider]** namesilo: fix cleanup. -## v3.8.0 +## [v3.8.0] - 2020-07-02 -- Release date: 2020-07-02 -- Tag: [v3.8.0](https://github.com/go-acme/lego/releases/tag/v3.8.0) - -### Added +### Added: - **[cli]** cli: add hook on the run command. - **[dnsprovider]** inwx: Two-Factor-Authentication - **[dnsprovider]** Add DNS provider for ArvanCloud -### Changed +### Changed: - **[dnsprovider]** vultr: bumping govultr version - **[dnsprovider]** desec: improve error logs. - **[lib]** Ensures the return of a location during account updates - **[dnsprovider]** route53: Document all AWS credential environment variables -### Fixed +### Fixed: - **[dnsprovider]** stackpath: fix subdomain support. - **[dnsprovider]** arvandcloud: fix record name. @@ -1237,12 +580,9 @@ Cancelled due to a CI issue, replaced by v4.5.2. - **[dnsprovider]** hetzner: fix record name. - **[lib]** Registrar.ResolveAccountByKey: Fix malformed request -## v3.7.0 +## [v3.7.0] - 2020-05-11 -- Release date: 2020-05-11 -- Tag: [v3.7.0](https://github.com/go-acme/lego/releases/tag/v3.7.0) - -### Added +### Added: - **[dnsprovider]** Add DNS provider for Netlify. - **[dnsprovider]** Add DNS provider for deSEC.io @@ -1251,31 +591,28 @@ Cancelled due to a CI issue, replaced by v4.5.2. - **[dnsprovider]** Add DNS provider for Mythic beasts DNSv2 - **[dnsprovider]** Add DNS provider for Yandex. -### Changed +### Changed: - **[dnsprovider]** Upgrade DNSimple client to 0.60.0 - **[dnsprovider]** update aws sdk -### Fixed +### Fixed: - **[dnsprovider]** autodns: removes TXT records during CleanUp. - **[dnsprovider]** Fix exoscale HTTP timeout - **[cli]** fix: renew path information. - **[cli]** Fix account storage location warning message -## v3.6.0 +## [v3.6.0] - 2020-04-24 -- Release date: 2020-04-24 -- Tag: [v3.6.0](https://github.com/go-acme/lego/releases/tag/v3.6.0) - -### Added +### Added: - **[dnsprovider]** Add DNS provider for CloudDNS. - **[dnsprovider]** alicloud: add support for domain with punycode - **[dnsprovider]** cloudns: Add subuser support - **[cli]** Information about renewed certificates are now passed to the renew hook -### Changed +### Changed: - **[dnsprovider]** acmedns: Update cpu/goacmedns v0.0.1 -> v0.0.2 - **[dnsprovider]** alicloud: update sdk dependency version to v1.61.112 @@ -1285,17 +622,14 @@ Cancelled due to a CI issue, replaced by v4.5.2. - **[dnsprovider]** namedotcom: get the actual registered domain, so we can remove just that from the hostname to be created - **[dnsprovider]** transip: updated the client to v6 -### Fixed +### Fixed: - **[dnsprovider]** ns1: fix missing domain in log - **[dnsprovider]** rimuhosting: use HTTP client from config. -## v3.5.0 +## [v3.5.0] - 2020-03-15 -- Release date: 2020-03-15 -- Tag: [v3.5.0](https://github.com/go-acme/lego/releases/tag/v3.5.0) - -### Added +### Added: - **[dnsprovider]** Add DNS provider for Dynu. - **[dnsprovider]** Add DNS provider for reg.ru @@ -1305,30 +639,27 @@ Cancelled due to a CI issue, replaced by v4.5.2. - **[cli]** Multi-arch Docker image. - **[cli]** Adds `--name` flag to list command. -### Changed +### Changed: - **[lib]** lib: Improve cleanup log messages. - **[lib]** Wrap errors. -### Fixed +### Fixed: - **[dnsprovider]** azure: pass AZURE_CLIENT_SECRET_FILE to autorest.Authorizer - **[dnsprovider]** gcloud: fixes issues when used with GKE Workload Identity - **[dnsprovider]** oraclecloud: fix subdomain support -## v3.4.0 +## [v3.4.0] - 2020-02-25 -- Release date: 2020-02-25 -- Tag: [v3.4.0](https://github.com/go-acme/lego/releases/tag/v3.4.0) - -### Added +### Added: - **[dnsprovider]** Add DNS provider for Constellix - **[dnsprovider]** Add DNS provider for Servercow. - **[dnsprovider]** Add DNS provider for Scaleway - **[cli]** Add "LEGO_PATH" environment variable -### Changed +### Changed: - **[dnsprovider]** route53: allow custom client to be provided - **[dnsprovider]** namecheap: allow external domains @@ -1336,7 +667,7 @@ Cancelled due to a CI issue, replaced by v4.5.2. - **[dnsprovider]** ovh: Improve provider documentation - **[dnsprovider]** route53: Improve provider documentation -### Fixed +### Fixed: - **[dnsprovider]** zoneee: fix subdomains. - **[dnsprovider]** designate: Don't clean up managed records like SOA and NS @@ -1344,213 +675,147 @@ Cancelled due to a CI issue, replaced by v4.5.2. - **[lib]** crypto: Treat CommonName as optional - **[lib]** chore: update cenkalti/backoff to v4. -## v3.3.0 - -- Release date: 2020-01-08 -- Tag: [v3.3.0](https://github.com/go-acme/lego/releases/tag/v3.3.0) - -### Added +## [v3.3.0] - 2020-01-08 +### Added: - **[dnsprovider]** Add DNS provider for Checkdomain - **[lib]** Add support to update account -### Changed - +### Changed: - **[dnsprovider]** gcloud: Auto-detection of the project ID. - **[lib]** Successfully parse private key PEM blocks -### Fixed - +### Fixed: - **[dnsprovider]** Update dnspod, because of API breaking changes. -## v3.2.0 - -- Release date: 2019-11-10 -- Tag: [v3.2.0](https://github.com/go-acme/lego/releases/tag/v3.2.0) - -### Added +## [v3.2.0] - 2019-11-10 +### Added: - **[dnsprovider]** Add support for autodns -### Changed - +### Changed: - **[dnsprovider]** httpreq: Allow use environment vars from a `_FILE` file - **[lib]** Don't deactivate valid authorizations - **[lib]** Expose more SOA fields found by dns01.FindZoneByFqdn -### Fixed - +### Fixed: - **[dnsprovider]** use token as unique ID. -## v3.1.0 - -- Release date: 2019-10-07 -- Tag: [v3.1.0](https://github.com/go-acme/lego/releases/tag/v3.1.0) - -### Added +## [v3.1.0] - 2019-10-07 +### Added: - **[dnsprovider]** Add DNS provider for Liquid Web - **[dnsprovider]** cloudflare: add support for API tokens - **[cli]** feat: ease operation behind proxy servers -### Changed - +### Changed: - **[dnsprovider]** cloudflare: update client - **[dnsprovider]** linodev4: propagation timeout configuration. -### Fixed - +### Fixed: - **[dnsprovider]** ovh: fix int overflow. - **[dnsprovider]** bindman: fix client version. -## v3.0.2 - -- Release date: 2019-08-15 -- Tag: [v3.0.2](https://github.com/go-acme/lego/releases/tag/v3.0.2) - -### Fixed +## [v3.0.2] - 2019-08-15 +### Fixed: - Invalid pseudo version (related to Cloudflare client). -## v3.0.1 - -- Release date: 2019-08-14 -- Tag: [v3.0.1](https://github.com/go-acme/lego/releases/tag/v3.0.1) +## [v3.0.1] - 2019-08-14 There was a problem when creating the tag v3.0.1, this tag has been invalidated. -## v3.0.0 - -- Release date: 2019-08-05 -- Tag: [v3.0.0](https://github.com/go-acme/lego/releases/tag/v3.0.0) - -### Changed +## [v3.0.0] - 2019-08-05 +### Changed: - migrate to go module (new import github.com/go-acme/lego/v3/) - update DNS clients -## v2.7.2 - -- Release date: 2019-07-30 -- Tag: [v2.7.2](https://github.com/go-acme/lego/releases/tag/v2.7.2) - -### Fixed +## [v2.7.2] - 2019-07-30 +### Fixed: - **[dnsprovider]** vultr: quote TXT record -## v2.7.1 - -- Release date: 2019-07-22 -- Tag: [v2.7.1](https://github.com/go-acme/lego/releases/tag/v2.7.1) - -### Fixed +## [v2.7.1] - 2019-07-22 +### Fixed: - **[dnsprovider]** vultr: invalid record type. -## v2.7.0 - -- Release date: 2019-07-17 -- Tag: [v2.7.0](https://github.com/go-acme/lego/releases/tag/v2.7.0) - -### Added +## [v2.7.0] - 2019-07-17 +### Added: - **[dnsprovider]** Add DNS provider for namesilo - **[dnsprovider]** Add DNS provider for versio.nl -### Changed - +### Changed: - **[dnsprovider]** Update DNS providers libs. - **[dnsprovider]** joker: support username and password. - **[dnsprovider]** Vultr: Switch to official client -### Fixed - +### Fixed: - **[dnsprovider]** otc: Prevent sending empty body. -## v2.6.0 - -- Release date: 2019-05-27 -- Tag: [v2.6.0](https://github.com/go-acme/lego/releases/tag/v2.6.0) - -### Added +## [v2.6.0] - 2019-05-27 +### Added: - **[dnsprovider]** Add support for Joker.com DMAPI - **[dnsprovider]** Add support for Bindman DNS provider - **[dnsprovider]** Add support for EasyDNS - **[lib]** Get an existing certificate by URL -### Changed - +### Changed: - **[dnsprovider]** digitalocean: LEGO_EXPERIMENTAL_CNAME_SUPPORT support - **[dnsprovider]** gcloud: Use fqdn to get zone Present/CleanUp - **[dnsprovider]** exec: serial behavior - **[dnsprovider]** manual: serial behavior. - **[dnsprovider]** Strip newlines when reading environment variables from `_FILE` suffixed files. -### Fixed - +### Fixed: - **[cli]** fix: cli disable-cp option. - **[dnsprovider]** gcloud: fix zone visibility. -## v2.5.0 - -- Release date: 2019-04-17 -- Tag: [v2.5.0](https://github.com/go-acme/lego/releases/tag/v2.5.0) - -### Added +## [v2.5.0] - 2019-04-17 +### Added: - **[cli]** Adds renew hook - **[dnsprovider]** Adds 'Since' to DNS providers documentation -### Changed - +### Changed: - **[dnsprovider]** gcloud: use public DNS zones - **[dnsprovider]** route53: enhance documentation. -### Fixed - +### Fixed: - **[dnsprovider]** cloudns: fix TTL and status validation - **[dnsprovider]** sakuracloud: supports concurrent update - **[dnsprovider]** Disable authz when solve fail. - Add tzdata to the Docker image. -## v2.4.0 +## [v2.4.0] - 2019-03-25 -- Release date: 2019-03-25 -- Tag: [v2.4.0](https://github.com/go-acme/lego/releases/tag/v2.4.0) - -Migrate from xenolf/lego to go-acme/lego. - -### Added +- Migrate from xenolf/lego to go-acme/lego. +### Added: - **[dnsprovider]** Add DNS Provider for Domain Offensive (do.de) - **[dnsprovider]** Adds information about '_FILE' suffix. -### Fixed - +### Fixed: - **[cli,dnsprovider]** Add 'manual' provider to the output of dnshelp - **[dnsprovider]** hostingde: Use provided ZoneName instead of domain - **[dnsprovider]** pdns: fix wildcard with SANs -## v2.3.0 - -- Release date: 2019-03-11 -- Tag: [v2.3.0](https://github.com/go-acme/lego/releases/tag/v2.3.0) - -### Added +## [v2.3.0] - 2019-03-11 +### Added: - **[dnsprovider]** Add DNS Provider for ClouDNS.net - **[dnsprovider]** Add DNS Provider for Oracle Cloud -### Changed - +### Changed: - **[cli]** Adds log when no renewal. - **[dnsprovider,lib]** Add a mechanism to wrap a PreCheckFunc - **[dnsprovider]** oraclecloud: better way to get private key. - **[dnsprovider]** exoscale: update library -### Fixed - +### Fixed: - **[dnsprovider]** OVH: Refresh zone after deleting challenge record - **[dnsprovider]** oraclecloud: ttl config and timeout - **[dnsprovider]** hostingde: fix client fails if customer has no access to dns-groups @@ -1559,56 +824,40 @@ Migrate from xenolf/lego to go-acme/lego. - **[dnsprovider]** vscale: fix TXT records clean up - **[dnsprovider]** selectel: fix TXT records clean up -## v2.2.0 - -- Release date: 2019-02-08 -- Tag: [v2.2.0](https://github.com/go-acme/lego/releases/tag/v2.2.0) - -### Added +## [v2.2.0] - 2019-02-08 +### Added: - **[dnsprovider]** Add support for Openstack Designate as a DNS provider - **[dnsprovider]** gcloud: Option to specify gcloud service account json by env as string - **[experimental feature]** Resolve CNAME when creating dns-01 challenge. To enable: set `LEGO_EXPERIMENTAL_CNAME_SUPPORT` to `true`. -### Changed - +### Changed: - **[cli]** Applies Let’s Encrypt’s recommendation about renew. The option `--days` of the command `renew` has a new default value (`30`) - **[lib]** Uses a jittered exponential backoff -### Fixed - +### Fixed: - **[cli]** CLI and key type. - **[dnsprovider]** httpreq: Endpoint with path. - **[dnsprovider]** fastdns: Do not overwrite existing TXT records - Log wildcard domain correctly in validation -## v2.1.0 - -- Release date: 2019-01-24 -- Tag: [v2.1.0](https://github.com/go-acme/lego/releases/tag/v2.1.0) - -### Added +## [v2.1.0] - 2019-01-24 +### Added: - **[dnsprovider]** Add support for zone.ee as a DNS provider. -### Changed - +### Changed: - **[dnsprovider]** nifcloud: Change DNS base url. - **[dnsprovider]** gcloud: More detailed information about Google Cloud DNS. -### Fixed - +### Fixed: - **[lib]** fix: OCSP, set HTTP client. - **[dnsprovider]** alicloud: fix pagination. - **[dnsprovider]** namecheap: fix panic. -## v2.0.0 - -- Release date: 2019-01-09 -- Tag: [v2.0.0](https://github.com/go-acme/lego/releases/tag/v2.0.0) - -### Added +## [v2.0.0] - 2019-01-09 +### Added: - **[cli,lib]** Option to disable the complete propagation Requirement - **[lib,cli]** Support non-ascii domain name (punnycode) - **[cli,lib]** Add configurable timeout when obtaining certificates @@ -1625,8 +874,7 @@ Migrate from xenolf/lego to go-acme/lego. - **[dnsprovider]** Add DNS Provider for inwx - **[dnsprovider]** alidns: add support to handle more than 20 domains -### Changed - +### Changed: - **[lib]** Check all challenges in a predictable order - **[lib]** Poll authz URL instead of challenge URL - **[lib]** Check all nameservers in a predictable order @@ -1641,15 +889,13 @@ Migrate from xenolf/lego to go-acme/lego. - **[cli]** the option `--days` of the command `renew` has default value (`15`) - **[dnsprovider]** gcloud: Use GCE_PROJECT for project always, if specified -### Removed - +### Removed: - **[lib]** Remove `SetHTTP01Address` - **[lib]** Remove `SetTLSALPN01Address` - **[lib]** Remove `Exclude` - **[cli]** Remove `--exclude`, `-x` -### Fixed - +### Fixed: - **[lib]** Fixes revocation for subdomains and non-ascii domains - **[lib]** Disable pending authorizations - **[dnsprovider]** transip: concurrent access to the API. @@ -1657,23 +903,17 @@ Migrate from xenolf/lego to go-acme/lego. - **[dnsprovider]** Azure: Do not overwrite existing TXT records - **[dnsprovider]** fix: Cloudflare error. -## v1.2.0 - -- Release date: 2018-11-04 -- Tag: [v1.2.0](https://github.com/go-acme/lego/releases/tag/v1.2.0) - -### Added +## [v1.2.0] - 2018-11-04 +### Added: - **[dnsprovider]** Add DNS Provider for ConoHa DNS - **[dnsprovider]** Add DNS Provider for MyDNS.jp - **[dnsprovider]** Add DNS Provider for Selectel -### Fixed - +### Fixed: - **[dnsprovider]** netcup: make unmarshalling of api-responses more lenient. -### Changed - +### Changed: - **[dnsprovider]** aurora: change DNS client - **[dnsprovider]** azure: update auth to support instance metadata service - **[dnsprovider]** dnsmadeeasy: log response body on error @@ -1681,13 +921,9 @@ Migrate from xenolf/lego to go-acme/lego. - **[lib]** Do not send a JWS body when POSTing challenges. - **[lib]** Support POST-as-GET. -## v1.1.0 - -- Release date: 2018-10-16 -- Tag: [v1.1.0](https://github.com/go-acme/lego/releases/tag/v1.1.0) - -### Added +## [v1.1.0] - 2018-10-16 +### Added: - **[lib]** TLS-ALPN-01 Challenge - **[cli]** Add filename parameter - **[dnsprovider]** Allow to configure TTL, interval and timeout @@ -1705,8 +941,7 @@ Migrate from xenolf/lego to go-acme/lego. - **[dnsprovider]** exec: add EXEC_MODE=RAW support. - **[dnsprovider]** cloudflare: support for CF_API_KEY and CF_API_EMAIL -### Fixed - +### Fixed: - **[lib]** Don't trust identifiers order. - **[lib]** Fix missing issuer certificates from Let's Encrypt - **[dnsprovider]** duckdns: fix TXT record update url @@ -1716,29 +951,20 @@ Migrate from xenolf/lego to go-acme/lego. - **[dnsprovider]** ns1: use the authoritative zone and not the domain name - **[dnsprovider]** ovh: check error to avoid panic due to nil client -### Changed - +### Changed: - **[lib]** Submit all dns records up front, then validate serially -## v1.0.0 - -- Release date: 2018-05-30 -- Tag: [v1.0.0](https://github.com/go-acme/lego/releases/tag/v1.0.0) - -### Changed +## [v1.0.0] - 2018-05-30 +### Changed: - **[lib]** ACME v2 Support. - **[dnsprovider]** Renamed `/providers/dns/googlecloud` to `/providers/dns/gcloud`. - **[dnsprovider]** Modified Google Cloud provider `gcloud.NewDNSProviderServiceAccount` function to extract the project id directly from the service account file. - **[dnsprovider]** Made errors more verbose for the Cloudflare provider. -## v0.5.0 - -- Release date: 2018-05-29 -- Tag: [v0.5.0](https://github.com/go-acme/lego/releases/tag/v0.5.0) - -### Added +## [v0.5.0] - 2018-05-29 +### Added: - **[dnsprovider]** Add DNS challenge provider `exec` - **[dnsprovider]** Add DNS Provider for Akamai FastDNS - **[dnsprovider]** Add DNS Provider for Bluecat DNS @@ -1750,8 +976,7 @@ Migrate from xenolf/lego to go-acme/lego. - **[dnsprovider]** Add DNS Provider for Lightsail - **[dnsprovider]** Add DNS Provider for Name.com -### Fixed - +### Fixed: - **[dnsprovider]** Azure: Added missing environment variable in the comments - **[dnsprovider]** PowerDNS: Fix zone URL, add leading slash. - **[dnsprovider]** DNSimple: Fix api @@ -1760,8 +985,7 @@ Migrate from xenolf/lego to go-acme/lego. - **[lib]** Fix zone detection for cross-zone cnames. - **[lib]** Use proxies from environment when making outbound http connections. -### Changed - +### Changed: - **[lib]** Users of an effective top-level domain can use the DNS challenge. - **[dnsprovider]** Azure: Refactor to work with new Azure SDK version. - **[dnsprovider]** Cloudflare and Azure: Adding output of which envvars are missing. @@ -1769,29 +993,20 @@ Migrate from xenolf/lego to go-acme/lego. - **[dnsprovider]** Exoscale: update to latest egoscale version. - **[dnsprovider]** Route53: Use NewSessionWithOptions instead of deprecated New. -## 0.4.1 - -- Release date: 2017-09-26 -- Tag: [0.4.1](https://github.com/go-acme/lego/releases/tag/0.4.1) - -### Added +## [0.4.1] - 2017-09-26 +### Added: - lib: A new DNS provider for OTC. - lib: The `AWS_HOSTED_ZONE_ID` environment variable for the Route53 DNS provider to directly specify the zone. - lib: The `RFC2136_TIMEOUT` environment variable to make the timeout for the RFC2136 provider configurable. - lib: The `GCE_SERVICE_ACCOUNT_FILE` environment variable to specify a service account file for the Google Cloud DNS provider. -### Fixed - +### Fixed: - lib: Fixed an authentication issue with the latest Azure SDK. -## 0.4.0 - -- Release date: 2017-07-13 -- Tag: [0.4.0](https://github.com/go-acme/lego/releases/tag/0.4.0) - -### Added +## [0.4.0] - 2017-07-13 +### Added: - CLI: The `--http-timeout` switch. This allows for an override of the default client HTTP timeout. - lib: The `HTTPClient` field. This allows for an override of the default HTTP timeout for library HTTP requests. - CLI: The `--dns-timeout` switch. This allows for an override of the default DNS timeout for library DNS requests. @@ -1817,17 +1032,14 @@ Migrate from xenolf/lego to go-acme/lego. - lib: A new DNS provider for Exoscale DNS. - lib: A new DNS provider for DNSPod. -### Changed - +### Changed: - lib: Exported the `PreCheckDNS` field so library users can manage the DNS check in tests. - lib: The library will now skip challenge solving if a valid Authz already exists. -### Removed - +### Removed: - lib: The library will no longer check for auto-renewed certificates. This has been removed from the spec and is not supported in Boulder. -### Fixed - +### Fixed: - lib: Fix a problem with the Route53 provider where it was possible the verification was published to a private zone. - lib: Loading an account from file should fail if an integral part is nil - lib: Fix a potential issue where the Dyn provider could resolve to an incorrect zone. @@ -1841,28 +1053,20 @@ Migrate from xenolf/lego to go-acme/lego. - lib: Fixed a condition where we could stall due to an early error condition. - lib: Fixed an issue where Authz object could end up in an active state after an error condition. -## 0.3.1 - -- Release date: 2016-04-19 -- Tag: [0.3.1](https://github.com/go-acme/lego/releases/tag/0.3.1) - -### Added +## [0.3.1] - 2016-04-19 +### Added: - lib: A new DNS provider for Vultr. -### Fixed - +### Fixed: - lib: DNS Provider for DigitalOcean could not handle subdomains properly. - lib: handleHTTPError should only try to JSON decode error messages with the right content type. - lib: The propagation checker for the DNS challenge would not retry on send errors. -## 0.3.0 -- Release date: 2016-03-19 -- Tag: [0.3.0](https://github.com/go-acme/lego/releases/tag/0.3.0) - -### Added +## [0.3.0] - 2016-03-19 +### Added: - CLI: The `--dns` switch. To include the DNS challenge for consideration. When using this switch, all other solvers are disabled. Supported are the following solvers: cloudflare, digitalocean, dnsimple, dyn, gandi, googlecloud, namecheap, route53, rfc2136 and manual. - CLI: The `--accept-tos` switch. Indicates your acceptance of the Let's Encrypt terms of service without prompting you. - CLI: The `--webroot` switch. The HTTP-01 challenge may now be completed by dropping a file into a webroot. When using this switch, all other solvers are disabled. @@ -1877,7 +1081,6 @@ Migrate from xenolf/lego to go-acme/lego. - lib: The `acme.KeyType` type was added and is used for the configuration of crypto parameters for RSA and EC keys. Valid KeyTypes are: EC256, EC384, RSA2048, RSA4096 and RSA8192. ### Changed - - lib: ExcludeChallenges now expects to be passed an array of `Challenge` types. - lib: HTTP-01 now supports custom solvers using the `ChallengeProvider` interface. - lib: TLS-SNI-01 now supports custom solvers using the `ChallengeProvider` interface. @@ -1885,22 +1088,16 @@ Migrate from xenolf/lego to go-acme/lego. - lib: The `acme.NewClient` function now expects an `acme.KeyType` instead of the keyBits parameter. ### Removed - - CLI: The `rsa-key-size` switch was removed in favor of `key-type` to support EC keys. ### Fixed - - lib: Fixed a race condition in HTTP-01 - lib: Fixed an issue where status codes on ACME challenge responses could lead to no action being taken. - lib: Fixed a regression when calling the Renew function with a SAN certificate. -## 0.2.0 - -- Release date: 2016-01-09 -- Tag: [0.2.0](https://github.com/go-acme/lego/releases/tag/0.2.0) - -### Added +## [0.2.0] - 2016-01-09 +### Added: - CLI: The `--exclude` or `-x` switch. To exclude a challenge from being solved. - CLI: The `--http` switch. To set the listen address and port of HTTP based challenges. Supports `host:port` and `:port` for any interface. - CLI: The `--tls` switch. To set the listen address and port of TLS based challenges. Supports `host:port` and `:port` for any interface. @@ -1910,49 +1107,41 @@ Migrate from xenolf/lego to go-acme/lego. - lib: SetTLSAddress function. Pass a port to set the listen port of TLS based challenges. - lib: acme.UserAgent variable. Use this to customize the user agent on all requests sent by lego. -### Changed - +### Changed: - lib: NewClient does no longer accept the optPort parameter - lib: ObtainCertificate now returns a SAN certificate if you pass more than one domain. - lib: GetOCSPForCert now returns the parsed OCSP response instead of just the status. - lib: ObtainCertificate has a new parameter `privKey crypto.PrivateKey` which lets you reuse an existing private key for new certificates. - lib: RenewCertificate now expects the PrivateKey property of the CertificateResource to be set only if you want to reuse the key. -### Removed - +### Removed: - CLI: The `--port` switch was removed. - lib: RenewCertificate does no longer offer to also revoke your old certificate. -### Fixed - +### Fixed: - CLI: Fix logic using the `--days` parameter for renew -## 0.1.1 - -- Release date: 2015-12-18 -- Tag: [0.1.1](https://github.com/go-acme/lego/releases/tag/0.1.1) - -### Added +## [0.1.1] - 2015-12-18 +### Added: - CLI: Added a way to automate renewal through a cronjob using the --days parameter to renew -### Changed - +### Changed: - lib: Improved log output on challenge failures. -### Fixed - +### Fixed: - CLI: The short parameter for domains would not get accepted - CLI: The cli did not return proper exit codes on error library errors. - lib: RenewCertificate did not properly renew SAN certificates. ### Security - - lib: Fix possible DOS on GetOCSPForCert -## 0.1.0 +## [0.1.0] - 2015-12-03 +- Initial release -- Release date: 2015-12-03 -- Tag: [0.1.0](https://github.com/go-acme/lego/releases/tag/0.1.0) - -Initial release +[0.3.1]: https://github.com/go-acme/lego/compare/v0.3.0...v0.3.1 +[0.3.0]: https://github.com/go-acme/lego/compare/v0.2.0...v0.3.0 +[0.2.0]: https://github.com/go-acme/lego/compare/v0.1.1...v0.2.0 +[0.1.1]: https://github.com/go-acme/lego/compare/v0.1.0...v0.1.1 +[0.1.0]: https://github.com/go-acme/lego/tree/v0.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 05e4fa994..a0005cff8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ To ensure a great and easy experience for everyone, please review the few guidel - If both of the above do not apply, create a new issue and include as much information as possible. Bug reports should include all information a person could need to reproduce your problem without the need to -follow up for more information. If possible, provide detailed steps for us to reproduce it, the expected behavior and the actual behavior. +follow up for more information. If possible, provide detailed steps for us to reproduce it, the expected behaviour and the actual behaviour. ## Feature proposals and requests @@ -20,26 +20,31 @@ It is up to you to make a strong point about your proposal and convince us of th ## Pull requests -Create an issue and wait for a maintainer to approve it BEFORE opening a pull request. - Patches, new features and improvements are a great way to help the project. Please keep them focused on one thing and do not include unrelated commits. -All pull requests that alter the behavior of the program, -add new behavior or somehow alter code in a non-trivial way should **always** include tests. +All pull requests which alter the behaviour of the program, add new behaviour or somehow alter code in a non-trivial way should **always** include tests. -**IMPORTANT**: By submitting a patch, you agree to allow the project owners to license your work under the terms of the [MIT License](LICENSE). +If you want to contribute a significant pull request (with a non-trivial workload for you) please **ask first**. We do not want you to spend +a lot of time on something the project's developers might not want to merge into the project. + +**IMPORTANT**: By submitting a patch, you agree to allow the project +owners to license your work under the terms of the [MIT License](LICENSE). ### How to create a pull request Requirements: -- `go` v1.24+ +- `go` v1.15+ - environment variable: `GO111MODULE=on` First, you have to install [GoLang](https://golang.org/doc/install) and [golangci-lint](https://github.com/golangci/golangci-lint#install). ```bash +# Create the root folder +mkdir -p $GOPATH/src/github.com/go-acme +cd $GOPATH/src/github.com/go-acme + # clone your fork git clone git@github.com:YOUR_USERNAME/lego.git cd lego @@ -51,12 +56,14 @@ git fetch upstream ```bash # Create your branch -git switch -c my-feature +git checkout -b my-feature ## Create your code ## ``` ```bash +# Format +make fmt # Linters make checks # Tests diff --git a/LICENSE b/LICENSE index d8eaf915d..270cba089 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,5 @@ The MIT License (MIT) -Copyright (c) 2017-2024 Ludovic Fernandez Copyright (c) 2015-2017 Sebastian Erhart Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/Makefile b/Makefile index 8536dfc40..bd341e1a3 100644 --- a/Makefile +++ b/Makefile @@ -39,25 +39,25 @@ checks: .PHONY: patch minor major detach patch: - go run ./internal/releaser/ release -m patch + go run internal/release.go release -m patch minor: - go run ./internal/releaser/ release -m minor + go run internal/release.go release -m minor major: - go run ./internal/releaser/ release -m major + go run internal/release.go release -m major detach: - go run ./internal/releaser/ detach + go run internal/release.go detach # Docs .PHONY: docs-build docs-serve docs-themes docs-build: generate-dns - @make -C ./docs build + @make -C ./docs hugo-build docs-serve: generate-dns - @make -C ./docs serve + @make -C ./docs hugo docs-themes: @make -C ./docs hugo-themes diff --git a/README.md b/README.md index e90e94962..060497c24 100644 --- a/README.md +++ b/README.md @@ -5,36 +5,29 @@ # Lego -[ACME](https://www.rfc-editor.org/rfc/rfc8555.html) client and library for Let's Encrypt and other ACME CAs written in Go. +Let's Encrypt client and ACME library written in Go. [![Go Reference](https://pkg.go.dev/badge/github.com/go-acme/lego/v4.svg)](https://pkg.go.dev/github.com/go-acme/lego/v4) [![Build Status](https://github.com//go-acme/lego/workflows/Main/badge.svg?branch=master)](https://github.com//go-acme/lego/actions) [![Docker Pulls](https://img.shields.io/docker/pulls/goacme/lego.svg)](https://hub.docker.com/r/goacme/lego/) -lego is an independent, free, and open-source project, if you value it, consider [supporting it](https://donate.ldez.dev)! ❤️ - -Everybody thinks that the others will donate, but in the end, nobody does. - -So if you think that lego is worth it, please consider [donating](https://donate.ldez.dev). - ## Features - ACME v2 [RFC 8555](https://www.rfc-editor.org/rfc/rfc8555.html) - Support [RFC 8737](https://www.rfc-editor.org/rfc/rfc8737.html): TLS Application‑Layer Protocol Negotiation (ALPN) Challenge Extension - Support [RFC 8738](https://www.rfc-editor.org/rfc/rfc8738.html): certificates for IP addresses - - Support [RFC 9773](https://www.rfc-editor.org/rfc/rfc9773.html): Renewal Information (ARI) Extension - - Support [draft-ietf-acme-profiles-00](https://datatracker.ietf.org/doc/draft-ietf-acme-profiles/): Profiles Extension -- Comes with about [180 DNS providers](https://go-acme.github.io/lego/dns) + - Support [draft-ietf-acme-ari-03](https://datatracker.ietf.org/doc/draft-ietf-acme-ari/): Renewal Information (ARI) Extension - Register with CA - Obtain certificates, both from scratch or with an existing CSR - Renew certificates - Revoke certificates -- Robust implementation of ACME challenges: +- Robust implementation of all ACME challenges - HTTP (http-01) - DNS (dns-01) - TLS (tls-alpn-01) - SAN certificate support - [CNAME support](https://letsencrypt.org/2019/10/09/onboarding-your-customers-with-lets-encrypt-and-acme.html) by default +- Comes with multiple optional [DNS providers](https://go-acme.github.io/lego/dns) - [Custom challenge solvers](https://go-acme.github.io/lego/usage/library/writing-a-challenge-solver/) - Certificate bundling - OCSP helper function @@ -56,252 +49,44 @@ Documentation is hosted live at https://go-acme.github.io/lego/. Detailed documentation is available [here](https://go-acme.github.io/lego/dns). -If your DNS provider is not supported, please open an [issue](https://github.com/go-acme/lego/issues/new?assignees=&labels=enhancement%2C+new-provider&template=new_dns_provider.yml). - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
35.com/三五互联Active24Akamai EdgeDNSAlibaba Cloud DNS
AlibabaCloud ESAall-inklAlwaysdataAmazon Lightsail
Amazon Route 53Anexia CloudDNSANS SafeDNSArtFiles
ArvanCloudAurora DNSAutodnsAxelname
AzionAzure (deprecated)Azure DNSBaidu Cloud
Beget.comBinary LaneBindmanBluecat
Bluecat v2BookMyNameBrandit (deprecated)Bunny
CheckdomainCivoCloud.ruCloudDNS
CloudflareClouDNSCloudXNS (Deprecated)ConoHa v2
ConoHa v3ConstellixCore-NetworksCPanel/WHM
CzechiaDDnss (DynDNS Service)Derak ClouddeSEC.io
Designate DNSaaS for OpenstackDigital OceanDirectAdminDNS Made Easy
DNSExitdnsHome.deDNSimpleDNSPod (deprecated)
Domain Offensive (do.de)DomeneshopDreamHostDuck DNS
DynDynDnsFree.deDynuEasyDNS
EdgeCenterEfficient IPEpikEuroDNS
ExcedoExoscaleExternal programF5 XC
freemyip.comFusionLayer NameSurferG-CoreGandi
Gandi Live DNS (v5)Gigahost.noGlesysGo Daddy
Google CloudGoogle DomainsGravityHetzner
Hosting.deHosting.nlHostingerHosttech
HTTP requesthttp.netHuawei CloudHurricane Electric DNS
HyperOneIBM Cloud (SoftLayer)IIJ DNS Platform ServiceInfoblox
InfomaniakInternet Initiative JapanInternet.bsINWX
IonosIonos CloudIPv64ISPConfig 3
ISPConfig 3 - Dynamic DNS (DDNS) Moduleiwantmyname (Deprecated)JD CloudJoker
Joohoi's ACME-DNSKeyHelpLeasewebLiara
Lima-CityLinode (v4)Liquid WebLoopia
LuaDNSMail-in-a-BoxManageEngine CloudDNSManual
MetanameMetaregistrarmijn.hostMittwald
myaddr.{tools,dev,io}MyDNS.jpMythicBeastsName.com
NamecheapNamesiloNearlyFreeSpeech.NETNeodigit
NetcupNetlifyNicmanagerNIFCloud
NjallaNodionNS1Octenium
Open Telekom CloudOracle CloudOVHplesk.com
PorkbunPowerDNSRackspaceRain Yun/雨云
RcodeZeroreg.ruRegfishRFC2136
RimuHostingRU CENTERSakura CloudScaleway
SelectelSelectel v2SelfHost.(de|eu)Servercow
ShellrentSimply.comSonicSpaceship
StackpathSyseTechnitiumTencent Cloud DNS
Tencent EdgeOneTimeweb CloudTodayNIC/时代互联TransIP
UltradnsUnited-DomainsVariomediaVegaDNS
VercelVersio.[nl|eu|uk]VinylDNSVirtualname
VK CloudVolcano Engine/火山引擎VscaleVultr
webnames.cawebnames.ruWebsupportWEDOS
West.cn/西部数码Yandex 360Yandex CloudYandex PDD
Zone.eeZoneEditZonomi
+| | | | | +|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------| +| [Akamai EdgeDNS](https://go-acme.github.io/lego/dns/edgedns/) | [Alibaba Cloud DNS](https://go-acme.github.io/lego/dns/alidns/) | [all-inkl](https://go-acme.github.io/lego/dns/allinkl/) | [Amazon Lightsail](https://go-acme.github.io/lego/dns/lightsail/) | +| [Amazon Route 53](https://go-acme.github.io/lego/dns/route53/) | [ArvanCloud](https://go-acme.github.io/lego/dns/arvancloud/) | [Aurora DNS](https://go-acme.github.io/lego/dns/auroradns/) | [Autodns](https://go-acme.github.io/lego/dns/autodns/) | +| [Azure (deprecated)](https://go-acme.github.io/lego/dns/azure/) | [Azure DNS](https://go-acme.github.io/lego/dns/azuredns/) | [Bindman](https://go-acme.github.io/lego/dns/bindman/) | [Bluecat](https://go-acme.github.io/lego/dns/bluecat/) | +| [Brandit](https://go-acme.github.io/lego/dns/brandit/) | [Bunny](https://go-acme.github.io/lego/dns/bunny/) | [Checkdomain](https://go-acme.github.io/lego/dns/checkdomain/) | [Civo](https://go-acme.github.io/lego/dns/civo/) | +| [Cloud.ru](https://go-acme.github.io/lego/dns/cloudru/) | [CloudDNS](https://go-acme.github.io/lego/dns/clouddns/) | [Cloudflare](https://go-acme.github.io/lego/dns/cloudflare/) | [ClouDNS](https://go-acme.github.io/lego/dns/cloudns/) | +| [CloudXNS](https://go-acme.github.io/lego/dns/cloudxns/) | [ConoHa](https://go-acme.github.io/lego/dns/conoha/) | [Constellix](https://go-acme.github.io/lego/dns/constellix/) | [CPanel/WHM](https://go-acme.github.io/lego/dns/cpanel/) | +| [Derak Cloud](https://go-acme.github.io/lego/dns/derak/) | [deSEC.io](https://go-acme.github.io/lego/dns/desec/) | [Designate DNSaaS for Openstack](https://go-acme.github.io/lego/dns/designate/) | [Digital Ocean](https://go-acme.github.io/lego/dns/digitalocean/) | +| [DNS Made Easy](https://go-acme.github.io/lego/dns/dnsmadeeasy/) | [dnsHome.de](https://go-acme.github.io/lego/dns/dnshomede/) | [DNSimple](https://go-acme.github.io/lego/dns/dnsimple/) | [DNSPod (deprecated)](https://go-acme.github.io/lego/dns/dnspod/) | +| [Domain Offensive (do.de)](https://go-acme.github.io/lego/dns/dode/) | [Domeneshop](https://go-acme.github.io/lego/dns/domeneshop/) | [DreamHost](https://go-acme.github.io/lego/dns/dreamhost/) | [Duck DNS](https://go-acme.github.io/lego/dns/duckdns/) | +| [Dyn](https://go-acme.github.io/lego/dns/dyn/) | [Dynu](https://go-acme.github.io/lego/dns/dynu/) | [EasyDNS](https://go-acme.github.io/lego/dns/easydns/) | [Efficient IP](https://go-acme.github.io/lego/dns/efficientip/) | +| [Epik](https://go-acme.github.io/lego/dns/epik/) | [Exoscale](https://go-acme.github.io/lego/dns/exoscale/) | [External program](https://go-acme.github.io/lego/dns/exec/) | [freemyip.com](https://go-acme.github.io/lego/dns/freemyip/) | +| [G-Core](https://go-acme.github.io/lego/dns/gcore/) | [Gandi Live DNS (v5)](https://go-acme.github.io/lego/dns/gandiv5/) | [Gandi](https://go-acme.github.io/lego/dns/gandi/) | [Glesys](https://go-acme.github.io/lego/dns/glesys/) | +| [Go Daddy](https://go-acme.github.io/lego/dns/godaddy/) | [Google Cloud](https://go-acme.github.io/lego/dns/gcloud/) | [Google Domains](https://go-acme.github.io/lego/dns/googledomains/) | [Hetzner](https://go-acme.github.io/lego/dns/hetzner/) | +| [Hosting.de](https://go-acme.github.io/lego/dns/hostingde/) | [Hosttech](https://go-acme.github.io/lego/dns/hosttech/) | [HTTP request](https://go-acme.github.io/lego/dns/httpreq/) | [http.net](https://go-acme.github.io/lego/dns/httpnet/) | +| [Hurricane Electric DNS](https://go-acme.github.io/lego/dns/hurricane/) | [HyperOne](https://go-acme.github.io/lego/dns/hyperone/) | [IBM Cloud (SoftLayer)](https://go-acme.github.io/lego/dns/ibmcloud/) | [IIJ DNS Platform Service](https://go-acme.github.io/lego/dns/iijdpf/) | +| [Infoblox](https://go-acme.github.io/lego/dns/infoblox/) | [Infomaniak](https://go-acme.github.io/lego/dns/infomaniak/) | [Internet Initiative Japan](https://go-acme.github.io/lego/dns/iij/) | [Internet.bs](https://go-acme.github.io/lego/dns/internetbs/) | +| [INWX](https://go-acme.github.io/lego/dns/inwx/) | [Ionos](https://go-acme.github.io/lego/dns/ionos/) | [IPv64](https://go-acme.github.io/lego/dns/ipv64/) | [iwantmyname](https://go-acme.github.io/lego/dns/iwantmyname/) | +| [Joker](https://go-acme.github.io/lego/dns/joker/) | [Joohoi's ACME-DNS](https://go-acme.github.io/lego/dns/acme-dns/) | [Liara](https://go-acme.github.io/lego/dns/liara/) | [Linode (v4)](https://go-acme.github.io/lego/dns/linode/) | +| [Liquid Web](https://go-acme.github.io/lego/dns/liquidweb/) | [Loopia](https://go-acme.github.io/lego/dns/loopia/) | [LuaDNS](https://go-acme.github.io/lego/dns/luadns/) | [Mail-in-a-Box](https://go-acme.github.io/lego/dns/mailinabox/) | +| [Manual](https://go-acme.github.io/lego/dns/manual/) | [Metaname](https://go-acme.github.io/lego/dns/metaname/) | [MyDNS.jp](https://go-acme.github.io/lego/dns/mydnsjp/) | [MythicBeasts](https://go-acme.github.io/lego/dns/mythicbeasts/) | +| [Name.com](https://go-acme.github.io/lego/dns/namedotcom/) | [Namecheap](https://go-acme.github.io/lego/dns/namecheap/) | [Namesilo](https://go-acme.github.io/lego/dns/namesilo/) | [NearlyFreeSpeech.NET](https://go-acme.github.io/lego/dns/nearlyfreespeech/) | +| [Netcup](https://go-acme.github.io/lego/dns/netcup/) | [Netlify](https://go-acme.github.io/lego/dns/netlify/) | [Nicmanager](https://go-acme.github.io/lego/dns/nicmanager/) | [NIFCloud](https://go-acme.github.io/lego/dns/nifcloud/) | +| [Njalla](https://go-acme.github.io/lego/dns/njalla/) | [Nodion](https://go-acme.github.io/lego/dns/nodion/) | [NS1](https://go-acme.github.io/lego/dns/ns1/) | [Open Telekom Cloud](https://go-acme.github.io/lego/dns/otc/) | +| [Oracle Cloud](https://go-acme.github.io/lego/dns/oraclecloud/) | [OVH](https://go-acme.github.io/lego/dns/ovh/) | [plesk.com](https://go-acme.github.io/lego/dns/plesk/) | [Porkbun](https://go-acme.github.io/lego/dns/porkbun/) | +| [PowerDNS](https://go-acme.github.io/lego/dns/pdns/) | [Rackspace](https://go-acme.github.io/lego/dns/rackspace/) | [RcodeZero](https://go-acme.github.io/lego/dns/rcodezero/) | [reg.ru](https://go-acme.github.io/lego/dns/regru/) | +| [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/) | [RimuHosting](https://go-acme.github.io/lego/dns/rimuhosting/) | [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/) | [Scaleway](https://go-acme.github.io/lego/dns/scaleway/) | +| [Selectel v2](https://go-acme.github.io/lego/dns/selectelv2/) | [Selectel](https://go-acme.github.io/lego/dns/selectel/) | [Servercow](https://go-acme.github.io/lego/dns/servercow/) | [Shellrent](https://go-acme.github.io/lego/dns/shellrent/) | +| [Simply.com](https://go-acme.github.io/lego/dns/simply/) | [Sonic](https://go-acme.github.io/lego/dns/sonic/) | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/) | [Tencent Cloud DNS](https://go-acme.github.io/lego/dns/tencentcloud/) | +| [TransIP](https://go-acme.github.io/lego/dns/transip/) | [UKFast SafeDNS](https://go-acme.github.io/lego/dns/safedns/) | [Ultradns](https://go-acme.github.io/lego/dns/ultradns/) | [Variomedia](https://go-acme.github.io/lego/dns/variomedia/) | +| [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | [Vercel](https://go-acme.github.io/lego/dns/vercel/) | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/) | [VinylDNS](https://go-acme.github.io/lego/dns/vinyldns/) | +| [VK Cloud](https://go-acme.github.io/lego/dns/vkcloud/) | [Vscale](https://go-acme.github.io/lego/dns/vscale/) | [Vultr](https://go-acme.github.io/lego/dns/vultr/) | [Webnames](https://go-acme.github.io/lego/dns/webnames/) | +| [Websupport](https://go-acme.github.io/lego/dns/websupport/) | [WEDOS](https://go-acme.github.io/lego/dns/wedos/) | [Yandex 360](https://go-acme.github.io/lego/dns/yandex360/) | [Yandex Cloud](https://go-acme.github.io/lego/dns/yandexcloud/) | +| [Yandex PDD](https://go-acme.github.io/lego/dns/yandex/) | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/) | [Zonomi](https://go-acme.github.io/lego/dns/zonomi/) | | -If your DNS provider is not supported, please open an [issue](https://github.com/go-acme/lego/issues/new?assignees=&labels=enhancement%2C+new-provider&template=new_dns_provider.yml). +If your DNS provider is not supported, please open an [issue](https://github.com/go-acme/lego/issues/new?assignees=&labels=enhancement%2C+new-provider&template=new_dns_provider.md). diff --git a/acme/api/account.go b/acme/api/account.go index 62e5ef9a6..85de84ef3 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -13,7 +13,6 @@ type AccountService service // New Creates a new account. func (a *AccountService) New(req acme.Account) (acme.ExtendedAccount, error) { var account acme.Account - resp, err := a.core.post(a.core.GetDirectory().NewAccountURL, req, &account) location := getLocation(resp) @@ -30,9 +29,9 @@ func (a *AccountService) New(req acme.Account) (acme.ExtendedAccount, error) { // NewEAB Creates a new account with an External Account Binding. func (a *AccountService) NewEAB(accMsg acme.Account, kid, hmacEncoded string) (acme.ExtendedAccount, error) { - hmac, err := decodeEABHmac(hmacEncoded) + hmac, err := base64.RawURLEncoding.DecodeString(hmacEncoded) if err != nil { - return acme.ExtendedAccount{}, err + return acme.ExtendedAccount{}, fmt.Errorf("acme: could not decode hmac key: %w", err) } eabJWS, err := a.core.signEABContent(a.core.GetDirectory().NewAccountURL, kid, hmac) @@ -52,12 +51,10 @@ func (a *AccountService) Get(accountURL string) (acme.Account, error) { } var account acme.Account - _, err := a.core.postAsGet(accountURL, &account) if err != nil { return acme.Account{}, err } - return account, nil } @@ -68,7 +65,6 @@ func (a *AccountService) Update(accountURL string, req acme.Account) (acme.Accou } var account acme.Account - _, err := a.core.post(accountURL, req, &account) if err != nil { return acme.Account{}, err @@ -85,20 +81,5 @@ func (a *AccountService) Deactivate(accountURL string) error { req := acme.Account{Status: acme.StatusDeactivated} _, err := a.core.post(accountURL, req, nil) - return err } - -func decodeEABHmac(hmacEncoded string) ([]byte, error) { - hmac, errRaw := base64.RawURLEncoding.DecodeString(hmacEncoded) - if errRaw == nil { - return hmac, nil - } - - hmac, err := base64.URLEncoding.DecodeString(hmacEncoded) - if err == nil { - return hmac, nil - } - - return nil, fmt.Errorf("acme: could not decode hmac key: %w", errors.Join(errRaw, err)) -} diff --git a/acme/api/account_test.go b/acme/api/account_test.go deleted file mode 100644 index 16bd80741..000000000 --- a/acme/api/account_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package api - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func Test_decodeEABHmac(t *testing.T) { - testCases := []struct { - desc string - hmac string - }{ - { - desc: "RawURLEncoding", - hmac: "BAEDAgQCBQcGCAUDDDMBAAIRAwQhEjEFQVFhEyJxgTIGFJGhsUIjJBVSwWIzNHKC0UMHJZJT8OHx", - }, - { - desc: "URLEncoding", - hmac: "nKTo9Hu8fpCqWPXx-25LVbZrJWxcHISsr4qHrRR0j5U=", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - v, err := decodeEABHmac(test.hmac) - require.NoError(t, err) - - assert.NotEmpty(t, v) - }) - } -} diff --git a/acme/api/api.go b/acme/api/api.go index da1c94d1b..b8c9cf0c9 100644 --- a/acme/api/api.go +++ b/acme/api/api.go @@ -2,7 +2,6 @@ package api import ( "bytes" - "context" "crypto" "encoding/json" "errors" @@ -10,7 +9,7 @@ import ( "net/http" "time" - "github.com/cenkalti/backoff/v5" + "github.com/cenkalti/backoff/v4" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api/internal/nonces" "github.com/go-acme/lego/v4/acme/api/internal/secure" @@ -61,7 +60,7 @@ func New(httpClient *http.Client, userAgent, caDirURL, kid string, privateKey cr // post performs an HTTP POST request and parses the response body as JSON, // into the provided respBody object. -func (a *Core) post(uri string, reqBody, response any) (*http.Response, error) { +func (a *Core) post(uri string, reqBody, response interface{}) (*http.Response, error) { content, err := json.Marshal(reqBody) if err != nil { return nil, errors.New("failed to marshal message") @@ -72,44 +71,47 @@ func (a *Core) post(uri string, reqBody, response any) (*http.Response, error) { // postAsGet performs an HTTP POST ("POST-as-GET") request. // https://www.rfc-editor.org/rfc/rfc8555.html#section-6.3 -func (a *Core) postAsGet(uri string, response any) (*http.Response, error) { +func (a *Core) postAsGet(uri string, response interface{}) (*http.Response, error) { return a.retrievablePost(uri, []byte{}, response) } -func (a *Core) retrievablePost(uri string, content []byte, response any) (*http.Response, error) { - ctx := context.Background() - +func (a *Core) retrievablePost(uri string, content []byte, response interface{}) (*http.Response, error) { // during tests, allow to support ~90% of bad nonce with a minimum of attempts. bo := backoff.NewExponentialBackOff() bo.InitialInterval = 200 * time.Millisecond bo.MaxInterval = 5 * time.Second + bo.MaxElapsedTime = 20 * time.Second - operation := func() (*http.Response, error) { - resp, err := a.signedPost(uri, content, response) + var resp *http.Response + operation := func() error { + var err error + resp, err = a.signedPost(uri, content, response) if err != nil { // Retry if the nonce was invalidated var e *acme.NonceError if errors.As(err, &e) { - return resp, err + return err } - return resp, backoff.Permanent(err) + return backoff.Permanent(err) } - return resp, nil + return nil } notify := func(err error, duration time.Duration) { log.Infof("retry due to: %v", err) } - return backoff.Retry(ctx, operation, - backoff.WithBackOff(bo), - backoff.WithMaxElapsedTime(20*time.Second), - backoff.WithNotify(notify)) + err := backoff.RetryNotify(operation, bo, notify) + if err != nil { + return resp, err + } + + return resp, nil } -func (a *Core) signedPost(uri string, content []byte, response any) (*http.Response, error) { +func (a *Core) signedPost(uri string, content []byte, response interface{}) (*http.Response, error) { signedContent, err := a.jws.SignContent(uri, content) if err != nil { return nil, fmt.Errorf("failed to post JWS message: failed to sign content: %w", err) @@ -155,7 +157,6 @@ func getDirectory(do *sender.Doer, caDirURL string) (acme.Directory, error) { if dir.NewAccountURL == "" { return dir, errors.New("directory missing new registration URL") } - if dir.NewOrderURL == "" { return dir, errors.New("directory missing new order URL") } diff --git a/acme/api/authorization.go b/acme/api/authorization.go index 4195bd1fe..a9972aa94 100644 --- a/acme/api/authorization.go +++ b/acme/api/authorization.go @@ -15,12 +15,10 @@ func (c *AuthorizationService) Get(authzURL string) (acme.Authorization, error) } var authz acme.Authorization - _, err := c.core.postAsGet(authzURL, &authz) if err != nil { return acme.Authorization{}, err } - return authz, nil } @@ -31,8 +29,6 @@ func (c *AuthorizationService) Deactivate(authzURL string) error { } var disabledAuth acme.Authorization - _, err := c.core.post(authzURL, acme.Authorization{Status: acme.StatusDeactivated}, &disabledAuth) - return err } diff --git a/acme/api/certificate.go b/acme/api/certificate.go index b42296768..5f31968cf 100644 --- a/acme/api/certificate.go +++ b/acme/api/certificate.go @@ -2,12 +2,15 @@ package api import ( "bytes" + "crypto/x509" "encoding/pem" "errors" "io" "net/http" "github.com/go-acme/lego/v4/acme" + "github.com/go-acme/lego/v4/certcrypto" + "github.com/go-acme/lego/v4/log" ) // maxBodySize is the maximum size of body that we will read. @@ -74,22 +77,62 @@ func (c *CertificateService) get(certURL string, bundle bool) (*acme.RawCertific return nil, resp.Header, err } - cert := c.getCertificateChain(data, bundle) + cert := c.getCertificateChain(data, resp.Header, bundle, certURL) return cert, resp.Header, err } // getCertificateChain Returns the certificate and the issuer certificate. -func (c *CertificateService) getCertificateChain(cert []byte, bundle bool) *acme.RawCertificate { +func (c *CertificateService) getCertificateChain(cert []byte, headers http.Header, bundle bool, certURL string) *acme.RawCertificate { // Get issuerCert from bundled response from Let's Encrypt // See https://community.letsencrypt.org/t/acme-v2-no-up-link-in-response/64962 _, issuer := pem.Decode(cert) + if issuer != nil { + // If bundle is false, we want to return a single certificate. + // To do this, we remove the issuer cert(s) from the issued cert. + if !bundle { + cert = bytes.TrimSuffix(cert, issuer) + } + return &acme.RawCertificate{Cert: cert, Issuer: issuer} + } - // If bundle is false, we want to return a single certificate. - // To do this, we remove the issuer cert(s) from the issued cert. - if !bundle { - cert = bytes.TrimSuffix(cert, issuer) + // The issuer certificate link may be supplied via an "up" link + // in the response headers of a new certificate. + // See https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4.2 + up := getLink(headers, "up") + + issuer, err := c.getIssuerFromLink(up) + if err != nil { + // If we fail to acquire the issuer cert, return the issued certificate - do not fail. + log.Warnf("acme: Could not bundle issuer certificate [%s]: %v", certURL, err) + } else if len(issuer) > 0 { + // If bundle is true, we want to return a certificate bundle. + // To do this, we append the issuer cert to the issued cert. + if bundle { + cert = append(cert, issuer...) + } } return &acme.RawCertificate{Cert: cert, Issuer: issuer} } + +// getIssuerFromLink requests the issuer certificate. +func (c *CertificateService) getIssuerFromLink(up string) ([]byte, error) { + if up == "" { + return nil, nil + } + + log.Infof("acme: Requesting issuer cert from %s", up) + + cert, _, err := c.get(up, false) + if err != nil { + return nil, err + } + + _, err = x509.ParseCertificate(cert.Cert) + if err != nil { + return nil, err + } + + return certcrypto.PEMEncode(certcrypto.DERCertificateBytes(cert.Cert)), nil +} diff --git a/acme/api/certificate_test.go b/acme/api/certificate_test.go index 7220ca1b9..9776cccc5 100644 --- a/acme/api/certificate_test.go +++ b/acme/api/certificate_test.go @@ -3,10 +3,11 @@ package api import ( "crypto/rand" "crypto/rsa" + "encoding/pem" + "net/http" "testing" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -73,34 +74,56 @@ rzFL1KZfz+HZdnFwFW2T2gVW8L3ii1l9AJDuKzlvjUH3p6bgihVq02sjT8mx+GM2 ` func TestCertificateService_Get_issuerRelUp(t *testing.T) { - server := tester.MockACMEServer(). - Route("POST /certificate", servermock.RawStringResponse(certResponseMock)). - BuildHTTPS(t) + mux, apiURL := tester.SetupFakeAPI(t) + + mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Link", "<"+apiURL+`/issuer>; rel="up"`) + _, err := w.Write([]byte(certResponseMock)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + mux.HandleFunc("/issuer", func(w http.ResponseWriter, _ *http.Request) { + p, _ := pem.Decode([]byte(issuerMock)) + _, err := w.Write(p.Bytes) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") - core, err := New(server.Client(), "lego-test", server.URL+"/dir", "", key) + core, err := New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) require.NoError(t, err) - cert, issuer, err := core.Certificates.Get(server.URL+"/certificate", true) + cert, issuer, err := core.Certificates.Get(apiURL+"/certificate", true) require.NoError(t, err) assert.Equal(t, certResponseMock, string(cert), "Certificate") assert.Equal(t, issuerMock, string(issuer), "IssuerCertificate") } func TestCertificateService_Get_embeddedIssuer(t *testing.T) { - server := tester.MockACMEServer(). - Route("POST /certificate", servermock.RawStringResponse(certResponseMock)). - BuildHTTPS(t) + mux, apiURL := tester.SetupFakeAPI(t) + + mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) { + _, err := w.Write([]byte(certResponseMock)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") - core, err := New(server.Client(), "lego-test", server.URL+"/dir", "", key) + core, err := New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) require.NoError(t, err) - cert, issuer, err := core.Certificates.Get(server.URL+"/certificate", true) + cert, issuer, err := core.Certificates.Get(apiURL+"/certificate", true) require.NoError(t, err) assert.Equal(t, certResponseMock, string(cert), "Certificate") assert.Equal(t, issuerMock, string(issuer), "IssuerCertificate") diff --git a/acme/api/challenge.go b/acme/api/challenge.go index 2af55fc1a..875dede6e 100644 --- a/acme/api/challenge.go +++ b/acme/api/challenge.go @@ -17,7 +17,6 @@ func (c *ChallengeService) New(chlgURL string) (acme.ExtendedChallenge, error) { // Challenge initiation is done by sending a JWS payload containing the trivial JSON object `{}`. // We use an empty struct instance as the postJSON payload here to achieve this result. var chlng acme.ExtendedChallenge - resp, err := c.core.post(chlgURL, struct{}{}, &chlng) if err != nil { return acme.ExtendedChallenge{}, err @@ -25,7 +24,6 @@ func (c *ChallengeService) New(chlgURL string) (acme.ExtendedChallenge, error) { chlng.AuthorizationURL = getLink(resp.Header, "up") chlng.RetryAfter = getRetryAfter(resp) - return chlng, nil } @@ -36,7 +34,6 @@ func (c *ChallengeService) Get(chlgURL string) (acme.ExtendedChallenge, error) { } var chlng acme.ExtendedChallenge - resp, err := c.core.postAsGet(chlgURL, &chlng) if err != nil { return acme.ExtendedChallenge{}, err @@ -44,6 +41,5 @@ func (c *ChallengeService) Get(chlgURL string) (acme.ExtendedChallenge, error) { chlng.AuthorizationURL = getLink(resp.Header, "up") chlng.RetryAfter = getRetryAfter(resp) - return chlng, nil } diff --git a/acme/api/identifier.go b/acme/api/identifier.go deleted file mode 100644 index 245ed8515..000000000 --- a/acme/api/identifier.go +++ /dev/null @@ -1,52 +0,0 @@ -package api - -import ( - "cmp" - "net" - "slices" - - "github.com/go-acme/lego/v4/acme" -) - -func createIdentifiers(domains []string) []acme.Identifier { - uniqIdentifiers := make(map[string]struct{}) - - var identifiers []acme.Identifier - - for _, domain := range domains { - if _, ok := uniqIdentifiers[domain]; ok { - continue - } - - ident := acme.Identifier{Value: domain, Type: "dns"} - - if net.ParseIP(domain) != nil { - ident.Type = "ip" - } - - identifiers = append(identifiers, ident) - - uniqIdentifiers[domain] = struct{}{} - } - - return identifiers -} - -// compareIdentifiers compares 2 slices of [acme.Identifier]. -func compareIdentifiers(a, b []acme.Identifier) int { - // Clones slices to avoid modifying original slices. - right := slices.Clone(a) - left := slices.Clone(b) - - slices.SortStableFunc(right, compareIdentifier) - slices.SortStableFunc(left, compareIdentifier) - - return slices.CompareFunc(right, left, compareIdentifier) -} - -func compareIdentifier(right, left acme.Identifier) int { - return cmp.Or( - cmp.Compare(right.Type, left.Type), - cmp.Compare(right.Value, left.Value), - ) -} diff --git a/acme/api/identifier_test.go b/acme/api/identifier_test.go deleted file mode 100644 index 586a87986..000000000 --- a/acme/api/identifier_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package api - -import ( - "testing" - - "github.com/go-acme/lego/v4/acme" - "github.com/stretchr/testify/assert" -) - -func Test_compareIdentifiers(t *testing.T) { - testCases := []struct { - desc string - a, b []acme.Identifier - expected int - }{ - { - desc: "identical identifiers", - a: []acme.Identifier{ - {Type: "dns", Value: "example.com"}, - {Type: "dns", Value: "*.example.com"}, - }, - b: []acme.Identifier{ - {Type: "dns", Value: "example.com"}, - {Type: "dns", Value: "*.example.com"}, - }, - expected: 0, - }, - { - desc: "identical identifiers but different order", - a: []acme.Identifier{ - {Type: "dns", Value: "example.com"}, - {Type: "dns", Value: "*.example.com"}, - }, - b: []acme.Identifier{ - {Type: "dns", Value: "*.example.com"}, - {Type: "dns", Value: "example.com"}, - }, - expected: 0, - }, - { - desc: "duplicate identifiers", - a: []acme.Identifier{ - {Type: "dns", Value: "example.com"}, - {Type: "dns", Value: "*.example.com"}, - }, - b: []acme.Identifier{ - {Type: "dns", Value: "example.com"}, - {Type: "dns", Value: "example.com"}, - }, - expected: -1, - }, - { - desc: "different identifier values", - a: []acme.Identifier{ - {Type: "dns", Value: "example.com"}, - {Type: "dns", Value: "*.example.com"}, - }, - b: []acme.Identifier{ - {Type: "dns", Value: "example.com"}, - {Type: "dns", Value: "*.example.org"}, - }, - expected: -1, - }, - { - desc: "different identifier types", - a: []acme.Identifier{ - {Type: "dns", Value: "example.com"}, - {Type: "dns", Value: "*.example.com"}, - }, - b: []acme.Identifier{ - {Type: "dns", Value: "example.com"}, - {Type: "ip", Value: "*.example.com"}, - }, - expected: -1, - }, - { - desc: "different number of identifiers a>b", - a: []acme.Identifier{ - {Type: "dns", Value: "example.com"}, - {Type: "dns", Value: "*.example.com"}, - {Type: "dns", Value: "example.org"}, - }, - b: []acme.Identifier{ - {Type: "dns", Value: "example.com"}, - {Type: "dns", Value: "*.example.com"}, - }, - expected: 1, - }, - { - desc: "different number of identifiers b>a", - a: []acme.Identifier{ - {Type: "dns", Value: "example.com"}, - {Type: "dns", Value: "*.example.com"}, - }, - b: []acme.Identifier{ - {Type: "dns", Value: "example.com"}, - {Type: "dns", Value: "*.example.com"}, - {Type: "dns", Value: "example.org"}, - }, - expected: -1, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - assert.Equal(t, test.expected, compareIdentifiers(test.a, test.b)) - }) - } -} diff --git a/acme/api/internal/nonces/nonce_manager.go b/acme/api/internal/nonces/nonce_manager.go index 04a4ac620..d089cf07c 100644 --- a/acme/api/internal/nonces/nonce_manager.go +++ b/acme/api/internal/nonces/nonce_manager.go @@ -11,11 +11,10 @@ import ( // Manager Manages nonces. type Manager struct { - sync.Mutex - do *sender.Doer nonceURL string nonces []string + sync.Mutex } // NewManager Creates a new Manager. @@ -37,7 +36,6 @@ func (n *Manager) Pop() (string, bool) { nonce := n.nonces[len(n.nonces)-1] n.nonces = n.nonces[:len(n.nonces)-1] - return nonce, true } @@ -45,7 +43,6 @@ func (n *Manager) Pop() (string, bool) { func (n *Manager) Push(nonce string) { n.Lock() defer n.Unlock() - n.nonces = append(n.nonces, nonce) } @@ -54,7 +51,6 @@ func (n *Manager) Nonce() (string, error) { if nonce, ok := n.Pop(); ok { return nonce, nil } - return n.getNonce() } diff --git a/acme/api/internal/nonces/nonce_manager_test.go b/acme/api/internal/nonces/nonce_manager_test.go index 4490165df..a172a0b69 100644 --- a/acme/api/internal/nonces/nonce_manager_test.go +++ b/acme/api/internal/nonces/nonce_manager_test.go @@ -8,52 +8,45 @@ import ( "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api/internal/sender" - "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/go-acme/lego/v4/platform/tester" ) func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) { - manager := servermock.NewBuilder( - func(server *httptest.Server) (*Manager, error) { - doer := sender.NewDoer(server.Client(), "lego-test") - - return NewManager(doer, server.URL), nil - }). - Route("HEAD /", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - time.Sleep(250 * time.Millisecond) - - rw.Header().Set("Replay-Nonce", "12345") - rw.Header().Set("Retry-After", "0") - - servermock.JSONEncode(&acme.Challenge{Type: "http-01", Status: "Valid", URL: "https://example.com/", Token: "token"}).ServeHTTP(rw, req) - })). - BuildHTTPS(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + time.Sleep(250 * time.Millisecond) + w.Header().Set("Replay-Nonce", "12345") + w.Header().Set("Retry-After", "0") + err := tester.WriteJSONResponse(w, &acme.Challenge{Type: "http-01", Status: "Valid", URL: "http://example.com/", Token: "token"}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + })) + t.Cleanup(server.Close) + doer := sender.NewDoer(http.DefaultClient, "lego-test") + j := NewManager(doer, server.URL) ch := make(chan bool) resultCh := make(chan bool) - go func() { - _, errN := manager.Nonce() + _, errN := j.Nonce() if errN != nil { t.Log(errN) } - ch <- true }() go func() { - _, errN := manager.Nonce() + _, errN := j.Nonce() if errN != nil { t.Log(errN) } - ch <- true }() go func() { <-ch <-ch - resultCh <- true }() - select { case <-resultCh: case <-time.After(500 * time.Millisecond): diff --git a/acme/api/internal/secure/jws.go b/acme/api/internal/secure/jws.go index 8cd598663..8afd44676 100644 --- a/acme/api/internal/secure/jws.go +++ b/acme/api/internal/secure/jws.go @@ -36,7 +36,6 @@ func (j *JWS) SetKid(kid string) { // SignContent Signs a content with the JWS. func (j *JWS) SignContent(url string, content []byte) (*jose.JSONWebSignature, error) { var alg jose.SignatureAlgorithm - switch k := j.privKey.(type) { case *rsa.PrivateKey: alg = jose.RS256 @@ -55,7 +54,7 @@ func (j *JWS) SignContent(url string, content []byte) (*jose.JSONWebSignature, e options := jose.SignerOptions{ NonceSource: j.nonces, - ExtraHeaders: map[jose.HeaderKey]any{ + ExtraHeaders: map[jose.HeaderKey]interface{}{ "url": url, }, } @@ -73,14 +72,12 @@ func (j *JWS) SignContent(url string, content []byte) (*jose.JSONWebSignature, e if err != nil { return nil, fmt.Errorf("failed to sign content: %w", err) } - return signed, nil } // SignEABContent Signs an external account binding content with the JWS. func (j *JWS) SignEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) { jwk := jose.JSONWebKey{Key: j.privKey} - jwkJSON, err := jwk.Public().MarshalJSON() if err != nil { return nil, fmt.Errorf("acme: error encoding eab jwk key: %w", err) @@ -90,7 +87,7 @@ func (j *JWS) SignEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignatu jose.SigningKey{Algorithm: jose.HS256, Key: hmac}, &jose.SignerOptions{ EmbedJWK: false, - ExtraHeaders: map[jose.HeaderKey]any{ + ExtraHeaders: map[jose.HeaderKey]interface{}{ "kid": kid, "url": url, }, @@ -111,7 +108,6 @@ func (j *JWS) SignEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignatu // GetKeyAuthorization Gets the key authorization for a token. func (j *JWS) GetKeyAuthorization(token string) (string, error) { var publicKey crypto.PublicKey - switch k := j.privKey.(type) { case *ecdsa.PrivateKey: publicKey = k.Public() diff --git a/acme/api/internal/secure/jws_test.go b/acme/api/internal/secure/jws_test.go index d033cb0c4..2e625f24f 100644 --- a/acme/api/internal/secure/jws_test.go +++ b/acme/api/internal/secure/jws_test.go @@ -9,52 +9,45 @@ import ( "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api/internal/nonces" "github.com/go-acme/lego/v4/acme/api/internal/sender" - "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/go-acme/lego/v4/platform/tester" ) func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) { - manager := servermock.NewBuilder( - func(server *httptest.Server) (*nonces.Manager, error) { - doer := sender.NewDoer(server.Client(), "lego-test") - - return nonces.NewManager(doer, server.URL), nil - }). - Route("HEAD /", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - time.Sleep(250 * time.Millisecond) - - rw.Header().Set("Replay-Nonce", "12345") - rw.Header().Set("Retry-After", "0") - - servermock.JSONEncode(&acme.Challenge{Type: "http-01", Status: "Valid", URL: "https://example.com/", Token: "token"}).ServeHTTP(rw, req) - })). - BuildHTTPS(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + time.Sleep(250 * time.Millisecond) + w.Header().Set("Replay-Nonce", "12345") + w.Header().Set("Retry-After", "0") + err := tester.WriteJSONResponse(w, &acme.Challenge{Type: "http-01", Status: "Valid", URL: "http://example.com/", Token: "token"}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + })) + t.Cleanup(server.Close) + doer := sender.NewDoer(http.DefaultClient, "lego-test") + j := nonces.NewManager(doer, server.URL) ch := make(chan bool) resultCh := make(chan bool) - go func() { - _, errN := manager.Nonce() + _, errN := j.Nonce() if errN != nil { t.Log(errN) } - ch <- true }() go func() { - _, errN := manager.Nonce() + _, errN := j.Nonce() if errN != nil { t.Log(errN) } - ch <- true }() go func() { <-ch <-ch - resultCh <- true }() - select { case <-resultCh: case <-time.After(500 * time.Millisecond): diff --git a/acme/api/internal/sender/sender.go b/acme/api/internal/sender/sender.go index d8859edf4..29cd7c9be 100644 --- a/acme/api/internal/sender/sender.go +++ b/acme/api/internal/sender/sender.go @@ -27,8 +27,6 @@ type Doer struct { // NewDoer Creates a new Doer. func NewDoer(client *http.Client, userAgent string) *Doer { - client.Transport = newHTTPSOnly(client) - return &Doer{ httpClient: client, userAgent: userAgent, @@ -37,7 +35,7 @@ func NewDoer(client *http.Client, userAgent string) *Doer { // Get performs a GET request with a proper User-Agent string. // If "response" is not provided, callers should close resp.Body when done reading from it. -func (d *Doer) Get(url string, response any) (*http.Response, error) { +func (d *Doer) Get(url string, response interface{}) (*http.Response, error) { req, err := d.newRequest(http.MethodGet, url, nil) if err != nil { return nil, err @@ -59,7 +57,7 @@ func (d *Doer) Head(url string) (*http.Response, error) { // Post performs a POST request with a proper User-Agent string. // If "response" is not provided, callers should close resp.Body when done reading from it. -func (d *Doer) Post(url string, body io.Reader, bodyType string, response any) (*http.Response, error) { +func (d *Doer) Post(url string, body io.Reader, bodyType string, response interface{}) (*http.Response, error) { req, err := d.newRequest(http.MethodPost, url, body, contentType(bodyType)) if err != nil { return nil, err @@ -86,7 +84,7 @@ func (d *Doer) newRequest(method, uri string, body io.Reader, opts ...RequestOpt return req, nil } -func (d *Doer) do(req *http.Request, response any) (*http.Response, error) { +func (d *Doer) do(req *http.Request, response interface{}) (*http.Response, error) { resp, err := d.httpClient.Do(req) if err != nil { return nil, err @@ -120,69 +118,31 @@ func (d *Doer) formatUserAgent() string { } func checkError(req *http.Request, resp *http.Response) error { - if resp.StatusCode < http.StatusBadRequest { - return nil - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("%d :: %s :: %s :: %w", resp.StatusCode, req.Method, req.URL, err) - } - - var errorDetails *acme.ProblemDetails - - err = json.Unmarshal(body, &errorDetails) - if err != nil { - return fmt.Errorf("%d ::%s :: %s :: %w :: %s", resp.StatusCode, req.Method, req.URL, err, string(body)) - } - - errorDetails.Method = req.Method - errorDetails.URL = req.URL.String() - - if errorDetails.HTTPStatus == 0 { - errorDetails.HTTPStatus = resp.StatusCode - } - - // Check for errors we handle specifically - switch { - case errorDetails.HTTPStatus == http.StatusBadRequest && errorDetails.Type == acme.BadNonceErr: - return &acme.NonceError{ProblemDetails: errorDetails} - - case errorDetails.HTTPStatus == http.StatusConflict && errorDetails.Type == acme.AlreadyReplacedErr: - return &acme.AlreadyReplacedError{ProblemDetails: errorDetails} - - case errorDetails.HTTPStatus == http.StatusTooManyRequests && errorDetails.Type == acme.RateLimitedErr: - return &acme.RateLimitedError{ - ProblemDetails: errorDetails, - RetryAfter: resp.Header.Get("Retry-After"), + if resp.StatusCode >= http.StatusBadRequest { + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("%d :: %s :: %s :: %w", resp.StatusCode, req.Method, req.URL, err) + } + + var errorDetails *acme.ProblemDetails + err = json.Unmarshal(body, &errorDetails) + if err != nil { + return fmt.Errorf("%d ::%s :: %s :: %w :: %s", resp.StatusCode, req.Method, req.URL, err, string(body)) + } + + errorDetails.Method = req.Method + errorDetails.URL = req.URL.String() + + if errorDetails.HTTPStatus == 0 { + errorDetails.HTTPStatus = resp.StatusCode + } + + // Check for errors we handle specifically + if errorDetails.HTTPStatus == http.StatusBadRequest && errorDetails.Type == acme.BadNonceErr { + return &acme.NonceError{ProblemDetails: errorDetails} } - default: return errorDetails } -} - -type httpsOnly struct { - rt http.RoundTripper -} - -func newHTTPSOnly(client *http.Client) *httpsOnly { - if client.Transport == nil { - return &httpsOnly{rt: http.DefaultTransport} - } - - return &httpsOnly{rt: client.Transport} -} - -// RoundTrip ensure HTTPS is used. -// Each ACME function is accomplished by the client sending a sequence of HTTPS requests to the server [RFC2818], -// carrying JSON messages [RFC8259]. -// Use of HTTPS is REQUIRED. -// https://datatracker.ietf.org/doc/html/rfc8555#section-6.1 -func (r *httpsOnly) RoundTrip(req *http.Request) (*http.Response, error) { - if req.URL.Scheme != "https" { - return nil, fmt.Errorf("HTTPS is required: %s", req.URL) - } - - return r.rt.RoundTrip(req) + return nil } diff --git a/acme/api/internal/sender/sender_test.go b/acme/api/internal/sender/sender_test.go index 73701ab11..2fd43c878 100644 --- a/acme/api/internal/sender/sender_test.go +++ b/acme/api/internal/sender/sender_test.go @@ -1,28 +1,24 @@ package sender import ( - "bytes" - "io" "net/http" "net/http/httptest" "strings" "testing" - "github.com/go-acme/lego/v4/acme" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestDo_UserAgentOnAllHTTPMethod(t *testing.T) { var ua, method string - - server := httptest.NewTLSServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { ua = r.Header.Get("User-Agent") method = r.Method })) t.Cleanup(server.Close) - doer := NewDoer(server.Client(), "") + doer := NewDoer(http.DefaultClient, "") testCases := []struct { method string @@ -64,87 +60,8 @@ func TestDo_CustomUserAgent(t *testing.T) { ua := doer.formatUserAgent() assert.Contains(t, ua, ourUserAgent) assert.Contains(t, ua, customUA) - if strings.HasSuffix(ua, " ") { t.Errorf("UA should not have trailing spaces; got '%s'", ua) } - assert.Len(t, strings.Split(ua, " "), 5) } - -func TestDo_failWithHTTP(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {})) - t.Cleanup(server.Close) - - sender := NewDoer(server.Client(), "test") - - _, err := sender.Post(server.URL, strings.NewReader("data"), "text/plain", nil) - require.ErrorContains(t, err, "HTTPS is required: http://") -} - -func Test_checkError(t *testing.T) { - testCases := []struct { - desc string - resp *http.Response - assert func(t *testing.T, err error) - }{ - { - desc: "default", - resp: &http.Response{ - StatusCode: http.StatusNotFound, - Body: io.NopCloser(bytes.NewBufferString(`{"type":"urn:ietf:params:acme:error:example","detail":"message","status":404}`)), - }, - assert: errorAs[*acme.ProblemDetails], - }, - { - desc: "badNonce", - resp: &http.Response{ - StatusCode: http.StatusBadRequest, - Body: io.NopCloser(bytes.NewBufferString(`{"type":"urn:ietf:params:acme:error:badNonce","detail":"message","status":400}`)), - }, - assert: errorAs[*acme.NonceError], - }, - { - desc: "alreadyReplaced", - resp: &http.Response{ - StatusCode: http.StatusConflict, - Body: io.NopCloser(bytes.NewBufferString(`{"type":"urn:ietf:params:acme:error:alreadyReplaced","detail":"message","status":409}`)), - }, - assert: errorAs[*acme.AlreadyReplacedError], - }, - { - desc: "rateLimited", - resp: &http.Response{ - StatusCode: http.StatusConflict, - Header: http.Header{ - "Retry-After": []string{"1"}, - }, - Body: io.NopCloser(bytes.NewBufferString(`{"type":"urn:ietf:params:acme:error:rateLimited","detail":"message","status":429}`)), - }, - assert: errorAs[*acme.RateLimitedError], - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "https://example.com", nil) - - err := checkError(req, test.resp) - require.Error(t, err) - - pb := &acme.ProblemDetails{} - assert.ErrorAs(t, err, &pb) - - test.assert(t, err) - }) - } -} - -func errorAs[T error](t *testing.T, err error) { - t.Helper() - - var zero T - assert.ErrorAs(t, err, &zero) -} diff --git a/acme/api/internal/sender/useragent.go b/acme/api/internal/sender/useragent.go index 51a1b4770..fd416f30e 100644 --- a/acme/api/internal/sender/useragent.go +++ b/acme/api/internal/sender/useragent.go @@ -1,13 +1,14 @@ -// Code generated by 'internal/releaser'; DO NOT EDIT. - package sender +// CODE GENERATED AUTOMATICALLY +// THIS FILE MUST NOT BE EDITED BY HAND + const ( // ourUserAgent is the User-Agent of this underlying library package. - ourUserAgent = "xenolf-acme/4.32.0" + ourUserAgent = "xenolf-acme/4.17.0" // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. - ourUserAgentComment = "detach" + ourUserAgentComment = "release" ) diff --git a/acme/api/order.go b/acme/api/order.go index fad6be2b8..5179d061a 100644 --- a/acme/api/order.go +++ b/acme/api/order.go @@ -3,8 +3,7 @@ package api import ( "encoding/base64" "errors" - "fmt" - "slices" + "net" "time" "github.com/go-acme/lego/v4/acme" @@ -14,15 +13,9 @@ import ( type OrderOptions struct { NotBefore time.Time NotAfter time.Time - - // A string uniquely identifying the profile - // which will be used to affect issuance of the certificate requested by this Order. - // - https://www.ietf.org/id/draft-ietf-acme-profiles-00.html#section-4 - Profile string - // A string uniquely identifying a previously-issued certificate which this // order is intended to replace. - // - https://www.rfc-editor.org/rfc/rfc9773.html#section-5 + // - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5 ReplacesCertID string } @@ -35,7 +28,18 @@ func (o *OrderService) New(domains []string) (acme.ExtendedOrder, error) { // NewWithOptions Creates a new order. func (o *OrderService) NewWithOptions(domains []string, opts *OrderOptions) (acme.ExtendedOrder, error) { - orderReq := acme.Order{Identifiers: createIdentifiers(domains)} + var identifiers []acme.Identifier + for _, domain := range domains { + ident := acme.Identifier{Value: domain, Type: "dns"} + + if net.ParseIP(domain) != nil { + ident.Type = "ip" + } + + identifiers = append(identifiers, ident) + } + + orderReq := acme.Order{Identifiers: identifiers} if opts != nil { if !opts.NotAfter.IsZero() { @@ -49,50 +53,12 @@ func (o *OrderService) NewWithOptions(domains []string, opts *OrderOptions) (acm if o.core.GetDirectory().RenewalInfo != "" { orderReq.Replaces = opts.ReplacesCertID } - - if opts.Profile != "" { - orderReq.Profile = opts.Profile - } } var order acme.Order - resp, err := o.core.post(o.core.GetDirectory().NewOrderURL, orderReq, &order) if err != nil { - are := &acme.AlreadyReplacedError{} - if !errors.As(err, &are) { - return acme.ExtendedOrder{}, err - } - - // If the Server rejects the request because the identified certificate has already been marked as replaced, - // it MUST return an HTTP 409 (Conflict) with a problem document of type "alreadyReplaced" (see Section 7.4). - // https://www.rfc-editor.org/rfc/rfc9773.html#section-5 - orderReq.Replaces = "" - - resp, err = o.core.post(o.core.GetDirectory().NewOrderURL, orderReq, &order) - if err != nil { - return acme.ExtendedOrder{}, err - } - } - - // The server MUST return an error if it cannot fulfill the request as specified, - // and it MUST NOT issue a certificate with contents other than those requested. - // If the server requires the request to be modified in a certain way, - // it should indicate the required changes using an appropriate error type and description. - // https://www.rfc-editor.org/rfc/rfc8555#section-7.4 - // - // Some ACME servers don't return an error, - // and/or change the order identifiers in the response, - // so we need to ensure that the identifiers are the same as requested. - // Deduplication by the server is allowed. - if compareIdentifiers(orderReq.Identifiers, order.Identifiers) != 0 { - // Sorts identifiers to avoid error message ambiguities about the order of the identifiers. - slices.SortStableFunc(orderReq.Identifiers, compareIdentifier) - slices.SortStableFunc(order.Identifiers, compareIdentifier) - - return acme.ExtendedOrder{}, - fmt.Errorf("order identifiers have been modified by the ACME server (RFC8555 §7.4): %+v != %+v", - orderReq.Identifiers, order.Identifiers) + return acme.ExtendedOrder{}, err } return acme.ExtendedOrder{ @@ -108,7 +74,6 @@ func (o *OrderService) Get(orderURL string) (acme.ExtendedOrder, error) { } var order acme.Order - _, err := o.core.postAsGet(orderURL, &order) if err != nil { return acme.ExtendedOrder{}, err @@ -124,14 +89,13 @@ func (o *OrderService) UpdateForCSR(orderURL string, csr []byte) (acme.ExtendedO } var order acme.Order - _, err := o.core.post(orderURL, csrMsg, &order) if err != nil { return acme.ExtendedOrder{}, err } if order.Status == acme.StatusInvalid { - return acme.ExtendedOrder{}, fmt.Errorf("invalid order: %w", order.Err()) + return acme.ExtendedOrder{}, order.Error } return acme.ExtendedOrder{Order: order}, nil diff --git a/acme/api/order_test.go b/acme/api/order_test.go index f74f473d2..26aaa3713 100644 --- a/acme/api/order_test.go +++ b/acme/api/order_test.go @@ -11,51 +11,55 @@ import ( "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/go-jose/go-jose/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestOrderService_NewWithOptions(t *testing.T) { + mux, apiURL := tester.SetupFakeAPI(t) + // small value keeps test fast - privateKey, errK := rsa.GenerateKey(rand.Reader, 1024) + privateKey, errK := rsa.GenerateKey(rand.Reader, 512) require.NoError(t, errK, "Could not generate test key") - server := tester.MockACMEServer(). - Route("POST /newOrder", - http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - body, err := readSignedBody(req, privateKey) - if err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) - return - } + mux.HandleFunc("/newOrder", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } - order := acme.Order{} + body, err := readSignedBody(r, privateKey) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } - err = json.Unmarshal(body, &order) - if err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) - return - } + order := acme.Order{} + err = json.Unmarshal(body, &order) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } - servermock.JSONEncode(acme.Order{ - Status: acme.StatusValid, - Expires: order.Expires, - Identifiers: order.Identifiers, - Profile: order.Profile, - NotBefore: order.NotBefore, - NotAfter: order.NotAfter, - Error: order.Error, - Authorizations: order.Authorizations, - Finalize: order.Finalize, - Certificate: order.Certificate, - Replaces: order.Replaces, - }).ServeHTTP(rw, req) - })). - BuildHTTPS(t) + err = tester.WriteJSONResponse(w, acme.Order{ + Status: acme.StatusValid, + Expires: order.Expires, + Identifiers: order.Identifiers, + NotBefore: order.NotBefore, + NotAfter: order.NotAfter, + Error: order.Error, + Authorizations: order.Authorizations, + Finalize: order.Finalize, + Certificate: order.Certificate, + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) - core, err := New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey) + core, err := New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) testCases := []struct { @@ -108,7 +112,6 @@ func readSignedBody(r *http.Request, privateKey *rsa.PrivateKey) ([]byte, error) } sigAlgs := []jose.SignatureAlgorithm{jose.RS256} - jws, err := jose.ParseSigned(string(reqBody), sigAlgs) if err != nil { return nil, err diff --git a/acme/api/renewal.go b/acme/api/renewal.go index aca3d8def..5b4046c69 100644 --- a/acme/api/renewal.go +++ b/acme/api/renewal.go @@ -14,7 +14,7 @@ var ErrNoARI = errors.New("renewalInfo[get/post]: server does not advertise a re // Note: this endpoint is part of a draft specification, not all ACME servers will implement it. // This method will return api.ErrNoARI if the server does not advertise a renewal info endpoint. // -// https://www.rfc-editor.org/rfc/rfc9773.html +// https://datatracker.ietf.org/doc/draft-ietf-acme-ari func (c *CertificateService) GetRenewalInfo(certID string) (*http.Response, error) { if c.core.GetDirectory().RenewalInfo == "" { return nil, ErrNoARI diff --git a/acme/api/service.go b/acme/api/service.go index 22ce05124..6f812ee03 100644 --- a/acme/api/service.go +++ b/acme/api/service.go @@ -1,11 +1,8 @@ package api import ( - "fmt" "net/http" "regexp" - "strconv" - "time" ) type service struct { @@ -26,13 +23,11 @@ func getLinks(header http.Header, rel string) []string { linkExpr := regexp.MustCompile(`<(.+?)>(?:;[^;]+)*?;\s*rel="(.+?)"`) var links []string - for _, link := range header["Link"] { for _, m := range linkExpr.FindAllStringSubmatch(link, -1) { if len(m) != 3 { continue } - if m[2] == rel { links = append(links, m[1]) } @@ -59,29 +54,3 @@ func getRetryAfter(resp *http.Response) string { return resp.Header.Get("Retry-After") } - -// ParseRetryAfter parses the Retry-After header value according to RFC 7231. -// The header can be either delay-seconds (numeric) or HTTP-date (RFC 1123 format). -// https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.3 -// Returns the duration until the retry time. -// TODO(ldez): unexposed this function in v5. -func ParseRetryAfter(value string) (time.Duration, error) { - if value == "" { - return 0, nil - } - - if seconds, err := strconv.ParseInt(value, 10, 64); err == nil { - return time.Duration(seconds) * time.Second, nil - } - - if retryTime, err := time.Parse(time.RFC1123, value); err == nil { - duration := time.Until(retryTime) - if duration < 0 { - return 0, nil - } - - return duration, nil - } - - return 0, fmt.Errorf("invalid Retry-After value: %q", value) -} diff --git a/acme/api/service_test.go b/acme/api/service_test.go index 57ea45708..2dbd795c9 100644 --- a/acme/api/service_test.go +++ b/acme/api/service_test.go @@ -3,10 +3,8 @@ package api import ( "net/http" "testing" - "time" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func Test_getLink(t *testing.T) { @@ -55,38 +53,3 @@ func Test_getLink(t *testing.T) { }) } } - -func TestParseRetryAfter(t *testing.T) { - testCases := []struct { - desc string - value string - expected time.Duration - }{ - { - desc: "empty header value", - value: "", - expected: time.Duration(0), - }, - { - desc: "delay-seconds", - value: "123", - expected: 123 * time.Second, - }, - { - desc: "HTTP-date", - value: time.Now().Add(3 * time.Second).Format(time.RFC1123), - expected: 3 * time.Second, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - rt, err := ParseRetryAfter(test.value) - require.NoError(t, err) - - assert.InDelta(t, test.expected.Seconds(), rt.Seconds(), 1) - }) - } -} diff --git a/acme/commons.go b/acme/commons.go index 0af623e4e..39aa35ac8 100644 --- a/acme/commons.go +++ b/acme/commons.go @@ -38,7 +38,7 @@ const ( // Directory the ACME directory object. // - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.1 -// - https://www.rfc-editor.org/rfc/rfc9773.html +// - https://datatracker.ietf.org/doc/draft-ietf-acme-ari/ type Directory struct { NewNonceURL string `json:"newNonce"` NewAccountURL string `json:"newAccount"` @@ -74,17 +74,11 @@ type Meta struct { // then the CA requires that all new-account requests include an "externalAccountBinding" field // associating the new account with an external account. ExternalAccountRequired bool `json:"externalAccountRequired"` - - // profiles (optional, object): - // A map of profile names to human-readable descriptions of those profiles. - // https://www.ietf.org/id/draft-ietf-acme-profiles-00.html#section-3 - Profiles map[string]string `json:"profiles"` } // ExtendedAccount an extended Account. type ExtendedAccount struct { Account - // Contains the value of the response header `Location` Location string `json:"-"` } @@ -154,12 +148,6 @@ type Order struct { // An array of identifier objects that the order pertains to. Identifiers []Identifier `json:"identifiers"` - // profile (string, optional): - // A string uniquely identifying the profile - // which will be used to affect issuance of the certificate requested by this Order. - // https://www.ietf.org/id/draft-ietf-acme-profiles-00.html#section-4 - Profile string `json:"profile,omitempty"` - // notBefore (optional, string): // The requested value of the notBefore field in the certificate, // in the date format defined in [RFC3339]. @@ -197,18 +185,10 @@ type Order struct { // replaces (optional, string): // replaces (string, optional): A string uniquely identifying a // previously-issued certificate which this order is intended to replace. - // - https://www.rfc-editor.org/rfc/rfc9773.html#section-5 + // - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5 Replaces string `json:"replaces,omitempty"` } -func (r *Order) Err() error { - if r.Error != nil { - return r.Error - } - - return nil -} - // Authorization the ACME authorization object. // - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.4 type Authorization struct { @@ -221,11 +201,11 @@ type Authorization struct { // The timestamp after which the server will consider this authorization invalid, // encoded in the format specified in RFC 3339 [RFC3339]. // This field is REQUIRED for objects with "valid" in the "status" field. - Expires time.Time `json:"expires,omitzero"` + Expires time.Time `json:"expires,omitempty"` // identifier (required, object): // The identifier that the account is authorized to represent - Identifier Identifier `json:"identifier"` + Identifier Identifier `json:"identifier,omitempty"` // challenges (required, array of objects): // For pending authorizations, the challenges that the client can fulfill in order to prove possession of the identifier. @@ -245,7 +225,6 @@ type Authorization struct { // ExtendedChallenge a extended Challenge. type ExtendedChallenge struct { Challenge - // Contains the value of the response header `Retry-After` RetryAfter string `json:"-"` // Contains the value of the response header `Link` rel="up" @@ -272,7 +251,7 @@ type Challenge struct { // The time at which the server validated this challenge, // encoded in the format specified in RFC 3339 [RFC3339]. // This field is REQUIRED if the "status" field is "valid". - Validated time.Time `json:"validated,omitzero"` + Validated time.Time `json:"validated,omitempty"` // error (optional, object): // Error that occurred while the server was validating the challenge, if any, @@ -295,14 +274,6 @@ type Challenge struct { KeyAuthorization string `json:"keyAuthorization"` } -func (c *Challenge) Err() error { - if c.Error != nil { - return c.Error - } - - return nil -} - // Identifier the ACME identifier object. // - https://www.rfc-editor.org/rfc/rfc8555.html#section-9.7.7 type Identifier struct { @@ -351,7 +322,7 @@ type Window struct { } // RenewalInfoResponse is the response to GET requests made the renewalInfo endpoint. -// - (4.1. Getting Renewal Information) https://www.rfc-editor.org/rfc/rfc9773.html +// - (4.1. Getting Renewal Information) https://datatracker.ietf.org/doc/draft-ietf-acme-ari/ type RenewalInfoResponse struct { // SuggestedWindow contains two fields, start and end, // whose values are timestamps which bound the window of time in which the CA recommends renewing the certificate. @@ -364,11 +335,11 @@ type RenewalInfoResponse struct { } // RenewalInfoUpdateRequest is the JWS payload for POST requests made to the renewalInfo endpoint. -// - (4.2. RenewalInfo Objects) https://www.rfc-editor.org/rfc/rfc9773.html#section-4.2 +// - (4.2. RenewalInfo Objects) https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-4.2 type RenewalInfoUpdateRequest struct { // CertID is a composite string in the format: base64url(AKI) || '.' || base64url(Serial), where AKI is the // certificate's authority key identifier and Serial is the certificate's serial number. For details, see: - // https://www.rfc-editor.org/rfc/rfc9773.html#section-4.1 + // https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-4.1 CertID string `json:"certID"` // Replaced is required and indicates whether or not the client considers the certificate to have been replaced. // A certificate is considered replaced when its revocation would not disrupt any ongoing services, diff --git a/acme/errors.go b/acme/errors.go index cd447d7b4..acaea5f65 100644 --- a/acme/errors.go +++ b/acme/errors.go @@ -2,15 +2,12 @@ package acme import ( "fmt" - "strings" ) // Errors types. const ( - errNS = "urn:ietf:params:acme:error:" - BadNonceErr = errNS + "badNonce" - AlreadyReplacedErr = errNS + "alreadyReplaced" - RateLimitedErr = errNS + "rateLimited" + errNS = "urn:ietf:params:acme:error:" + BadNonceErr = errNS + "badNonce" ) // ProblemDetails the problem details object. @@ -28,34 +25,30 @@ type ProblemDetails struct { URL string `json:"url,omitempty"` } -func (p *ProblemDetails) Error() string { - msg := new(strings.Builder) - - _, _ = fmt.Fprintf(msg, "acme: error: %d", p.HTTPStatus) - - if p.Method != "" || p.URL != "" { - _, _ = fmt.Fprintf(msg, " :: %s :: %s", p.Method, p.URL) - } - - _, _ = fmt.Fprintf(msg, " :: %s :: %s", p.Type, p.Detail) - - for _, sub := range p.SubProblems { - _, _ = fmt.Fprintf(msg, ", problem: %q :: %s", sub.Type, sub.Detail) - } - - if p.Instance != "" { - msg.WriteString(", url: " + p.Instance) - } - - return msg.String() -} - // SubProblem a "subproblems". // - https://www.rfc-editor.org/rfc/rfc8555.html#section-6.7.1 type SubProblem struct { Type string `json:"type,omitempty"` Detail string `json:"detail,omitempty"` - Identifier Identifier `json:"identifier"` + Identifier Identifier `json:"identifier,omitempty"` +} + +func (p ProblemDetails) Error() string { + msg := fmt.Sprintf("acme: error: %d", p.HTTPStatus) + if p.Method != "" || p.URL != "" { + msg += fmt.Sprintf(" :: %s :: %s", p.Method, p.URL) + } + msg += fmt.Sprintf(" :: %s :: %s", p.Type, p.Detail) + + for _, sub := range p.SubProblems { + msg += fmt.Sprintf(", problem: %q :: %s", sub.Type, sub.Detail) + } + + if p.Instance != "" { + msg += ", url: " + p.Instance + } + + return msg } // NonceError represents the error which is returned @@ -63,31 +56,3 @@ type SubProblem struct { type NonceError struct { *ProblemDetails } - -func (e *NonceError) Unwrap() error { - return e.ProblemDetails -} - -// AlreadyReplacedError represents the error which is returned -// if the Server rejects the request because the identified certificate has already been marked as replaced. -// - https://www.rfc-editor.org/rfc/rfc9773.html#section-5 -type AlreadyReplacedError struct { - *ProblemDetails -} - -func (e *AlreadyReplacedError) Unwrap() error { - return e.ProblemDetails -} - -// RateLimitedError represents the error which is returned -// if the server rejects the request because the client has exceeded the rate limit. -// - https://www.rfc-editor.org/rfc/rfc8555.html#section-6.6 -type RateLimitedError struct { - *ProblemDetails - - RetryAfter string -} - -func (e *RateLimitedError) Unwrap() error { - return e.ProblemDetails -} diff --git a/buildx.Dockerfile b/buildx.Dockerfile index 37f1dde94..92a86dd3d 100644 --- a/buildx.Dockerfile +++ b/buildx.Dockerfile @@ -1,12 +1,10 @@ # syntax=docker/dockerfile:1.4 FROM alpine:3 -ARG TARGETPLATFORM - RUN apk --no-cache --no-progress add git ca-certificates tzdata \ && rm -rf /var/cache/apk/* -COPY $TARGETPLATFORM/lego / +COPY lego / ENTRYPOINT ["/lego"] EXPOSE 80 diff --git a/certcrypto/crypto.go b/certcrypto/crypto.go index 800bb3f5b..43fa774ae 100644 --- a/certcrypto/crypto.go +++ b/certcrypto/crypto.go @@ -57,10 +57,8 @@ type DERCertificateBytes []byte // ParsePEMBundle parses a certificate bundle from top to bottom and returns // a slice of x509 certificates. This function will error if no certificates are found. func ParsePEMBundle(bundle []byte) ([]*x509.Certificate, error) { - var ( - certificates []*x509.Certificate - certDERBlock *pem.Block - ) + var certificates []*x509.Certificate + var certDERBlock *pem.Block for { certDERBlock, bundle = pem.Decode(bundle) @@ -73,7 +71,6 @@ func ParsePEMBundle(bundle []byte) ([]*x509.Certificate, error) { if err != nil { return nil, err } - certificates = append(certificates, cert) } } @@ -138,29 +135,10 @@ func GeneratePrivateKey(keyType KeyType) (crypto.PrivateKey, error) { return nil, fmt.Errorf("invalid KeyType: %s", keyType) } -// Deprecated: uses [CreateCSR] instead. func GenerateCSR(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) { - return CreateCSR(privateKey, CSROptions{ - Domain: domain, - SAN: san, - MustStaple: mustStaple, - }) -} - -type CSROptions struct { - Domain string - SAN []string - MustStaple bool - EmailAddresses []string -} - -func CreateCSR(privateKey crypto.PrivateKey, opts CSROptions) ([]byte, error) { - var ( - dnsNames []string - ipAddresses []net.IP - ) - - for _, altname := range opts.SAN { + var dnsNames []string + var ipAddresses []net.IP + for _, altname := range san { if ip := net.ParseIP(altname); ip != nil { ipAddresses = append(ipAddresses, ip) } else { @@ -169,13 +147,12 @@ func CreateCSR(privateKey crypto.PrivateKey, opts CSROptions) ([]byte, error) { } template := x509.CertificateRequest{ - Subject: pkix.Name{CommonName: opts.Domain}, - DNSNames: dnsNames, - EmailAddresses: opts.EmailAddresses, - IPAddresses: ipAddresses, + Subject: pkix.Name{CommonName: domain}, + DNSNames: dnsNames, + IPAddresses: ipAddresses, } - if opts.MustStaple { + if mustStaple { template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{ Id: tlsFeatureExtensionOID, Value: ocspMustStapleFeature, @@ -185,13 +162,12 @@ func CreateCSR(privateKey crypto.PrivateKey, opts CSROptions) ([]byte, error) { return x509.CreateCertificateRequest(rand.Reader, &template, privateKey) } -func PEMEncode(data any) []byte { +func PEMEncode(data interface{}) []byte { return pem.EncodeToMemory(PEMBlock(data)) } -func PEMBlock(data any) *pem.Block { +func PEMBlock(data interface{}) *pem.Block { var pemBlock *pem.Block - switch key := data.(type) { case *ecdsa.PrivateKey: keyBytes, _ := x509.MarshalECPrivateKey(key) @@ -242,15 +218,15 @@ func ParsePEMCertificate(cert []byte) (*x509.Certificate, error) { } func GetCertificateMainDomain(cert *x509.Certificate) (string, error) { - return getMainDomain(cert.Subject, cert.DNSNames, cert.IPAddresses) + return getMainDomain(cert.Subject, cert.DNSNames) } func GetCSRMainDomain(cert *x509.CertificateRequest) (string, error) { - return getMainDomain(cert.Subject, cert.DNSNames, cert.IPAddresses) + return getMainDomain(cert.Subject, cert.DNSNames) } -func getMainDomain(subject pkix.Name, dnsNames []string, ips []net.IP) (string, error) { - if subject.CommonName == "" && len(dnsNames) == 0 && len(ips) == 0 { +func getMainDomain(subject pkix.Name, dnsNames []string) (string, error) { + if subject.CommonName == "" && len(dnsNames) == 0 { return "", errors.New("missing domain") } @@ -258,11 +234,7 @@ func getMainDomain(subject pkix.Name, dnsNames []string, ips []net.IP) (string, return subject.CommonName, nil } - if len(dnsNames) > 0 { - return dnsNames[0], nil - } - - return ips[0].String(), nil + return dnsNames[0], nil } func ExtractDomains(cert *x509.Certificate) []string { @@ -276,7 +248,6 @@ func ExtractDomains(cert *x509.Certificate) []string { if sanDomain == cert.Subject.CommonName { continue } - domains = append(domains, sanDomain) } @@ -328,7 +299,6 @@ func GeneratePemCert(privateKey *rsa.PrivateKey, domain string, extensions []pki func generateDerCert(privateKey *rsa.PrivateKey, expiration time.Time, domain string, extensions []pkix.Extension) ([]byte, error) { serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) - serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { return nil, err diff --git a/certcrypto/crypto_test.go b/certcrypto/crypto_test.go index f5609fdf4..7aba8b378 100644 --- a/certcrypto/crypto_test.go +++ b/certcrypto/crypto_test.go @@ -6,6 +6,7 @@ import ( "crypto/rand" "crypto/rsa" "encoding/pem" + "regexp" "testing" "time" @@ -13,13 +14,6 @@ import ( "github.com/stretchr/testify/require" ) -const ( - testDomain1 = "lego.example" - testDomain2 = "a.lego.example" - testDomain3 = "b.lego.example" - testDomain4 = "c.lego.example" -) - func TestGeneratePrivateKey(t *testing.T) { key, err := GeneratePrivateKey(RSA2048) require.NoError(t, err, "Error generating private key") @@ -28,7 +22,7 @@ func TestGeneratePrivateKey(t *testing.T) { } func TestGenerateCSR(t *testing.T) { - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) + privateKey, err := rsa.GenerateKey(rand.Reader, 512) require.NoError(t, err, "Error generating private key") type expected struct { @@ -39,75 +33,55 @@ func TestGenerateCSR(t *testing.T) { testCases := []struct { desc string privateKey crypto.PrivateKey - opts CSROptions + domain string + san []string + mustStaple bool expected expected }{ { desc: "without SAN (nil)", privateKey: privateKey, - opts: CSROptions{ - Domain: testDomain1, - MustStaple: true, - }, - expected: expected{len: 382}, + domain: "lego.acme", + mustStaple: true, + expected: expected{len: 245}, }, { desc: "without SAN (empty)", privateKey: privateKey, - opts: CSROptions{ - Domain: testDomain1, - SAN: []string{}, - MustStaple: true, - }, - expected: expected{len: 382}, + domain: "lego.acme", + san: []string{}, + mustStaple: true, + expected: expected{len: 245}, }, { desc: "with SAN", privateKey: privateKey, - opts: CSROptions{ - Domain: testDomain1, - SAN: []string{testDomain2, testDomain3, testDomain4}, - MustStaple: true, - }, - expected: expected{len: 442}, + domain: "lego.acme", + san: []string{"a.lego.acme", "b.lego.acme", "c.lego.acme"}, + mustStaple: true, + expected: expected{len: 296}, }, { desc: "no domain", privateKey: privateKey, - opts: CSROptions{ - Domain: "", - MustStaple: true, - }, - expected: expected{len: 359}, + domain: "", + mustStaple: true, + expected: expected{len: 225}, }, { desc: "no domain with SAN", privateKey: privateKey, - opts: CSROptions{ - Domain: "", - SAN: []string{testDomain2, testDomain3, testDomain4}, - MustStaple: true, - }, - expected: expected{len: 419}, + domain: "", + san: []string{"a.lego.acme", "b.lego.acme", "c.lego.acme"}, + mustStaple: true, + expected: expected{len: 276}, }, { desc: "private key nil", privateKey: nil, - opts: CSROptions{ - Domain: testDomain1, - MustStaple: true, - }, - expected: expected{error: true}, - }, - { - desc: "with email addresses", - privateKey: privateKey, - opts: CSROptions{ - Domain: "example.com", - SAN: []string{"example.org"}, - EmailAddresses: []string{"foo@example.com", "bar@example.com"}, - }, - expected: expected{len: 421}, + domain: "fizz.buzz", + mustStaple: true, + expected: expected{error: true}, }, } @@ -115,7 +89,7 @@ func TestGenerateCSR(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - csr, err := CreateCSR(test.privateKey, test.opts) + csr, err := GenerateCSR(test.privateKey, test.domain, test.san, test.mustStaple) if test.expected.error { require.Error(t, err) @@ -130,17 +104,17 @@ func TestGenerateCSR(t *testing.T) { } func TestPEMEncode(t *testing.T) { - key, err := rsa.GenerateKey(rand.Reader, 1024) + buf := bytes.NewBufferString("TestingRSAIsSoMuchFun") + + reader := MockRandReader{b: buf} + key, err := rsa.GenerateKey(reader, 32) require.NoError(t, err, "Error generating private key") data := PEMEncode(key) require.NotNil(t, data) - p, rest := pem.Decode(data) - - assert.Equal(t, "RSA PRIVATE KEY", p.Type) - assert.Empty(t, rest) - assert.Empty(t, p.Headers) + exp := regexp.MustCompile(`^-----BEGIN RSA PRIVATE KEY-----\s+\S{60,}\s+-----END RSA PRIVATE KEY-----\s+`) + assert.Regexp(t, exp, string(data)) } func TestParsePEMCertificate(t *testing.T) { @@ -175,13 +149,10 @@ func TestParsePEMPrivateKey(t *testing.T) { pemPrivateKey := PEMEncode(privateKey) - // Decoding a key should work and create an identical RSA key to the original, - // ignoring precomputed values. + // Decoding a key should work and create an identical key to the original decoded, err := ParsePEMPrivateKey(pemPrivateKey) require.NoError(t, err) - - decodedRsaPrivateKey := decoded.(*rsa.PrivateKey) - require.True(t, decodedRsaPrivateKey.Equal(privateKey)) + assert.Equal(t, decoded, privateKey) // Decoding a PEM block that doesn't contain a private key should error _, err = ParsePEMPrivateKey(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE"})) @@ -195,3 +166,11 @@ func TestParsePEMPrivateKey(t *testing.T) { _, err = ParsePEMPrivateKey([]byte("This is not PEM")) require.Errorf(t, err, "Expected to return an error for non-PEM input") } + +type MockRandReader struct { + b *bytes.Buffer +} + +func (r MockRandReader) Read(p []byte) (int, error) { + return r.b.Read(p) +} diff --git a/certificate/authorization.go b/certificate/authorization.go index 49f958776..61c1244d9 100644 --- a/certificate/authorization.go +++ b/certificate/authorization.go @@ -7,10 +7,19 @@ import ( "github.com/go-acme/lego/v4/log" ) +const ( + // overallRequestLimit is the overall number of request per second + // limited on the "new-reg", "new-authz" and "new-cert" endpoints. + // From the documentation the limitation is 20 requests per second, + // but using 20 as value doesn't work but 18 do. + // https://letsencrypt.org/docs/rate-limits/ + overallRequestLimit = 18 +) + func (c *Certifier) getAuthorizations(order acme.ExtendedOrder) ([]acme.Authorization, error) { resc, errc := make(chan acme.Authorization), make(chan domainError) - delay := time.Second / time.Duration(c.overallRequestLimit) + delay := time.Second / overallRequestLimit for _, authzURL := range order.Authorizations { time.Sleep(delay) @@ -29,7 +38,6 @@ func (c *Certifier) getAuthorizations(order acme.ExtendedOrder) ([]acme.Authoriz var responses []acme.Authorization failures := newObtainError() - for range len(order.Authorizations) { select { case res := <-resc: @@ -53,7 +61,7 @@ func (c *Certifier) deactivateAuthorizations(order acme.ExtendedOrder, force boo for _, authzURL := range order.Authorizations { auth, err := c.core.Authorizations.Get(authzURL) if err != nil { - log.Infof("Unable to get the authorization for %s: %v", authzURL, err) + log.Infof("Unable to get the authorization for: %s", authzURL) continue } @@ -63,7 +71,6 @@ func (c *Certifier) deactivateAuthorizations(order acme.ExtendedOrder, force boo } log.Infof("Deactivating auth: %s", authzURL) - if c.core.Authorizations.Deactivate(authzURL) != nil { log.Infof("Unable to deactivate the authorization: %s", authzURL) } diff --git a/certificate/certificates.go b/certificate/certificates.go index 04904e794..7e69d1f4e 100644 --- a/certificate/certificates.go +++ b/certificate/certificates.go @@ -22,17 +22,6 @@ import ( "golang.org/x/net/idna" ) -const ( - // DefaultOverallRequestLimit is the overall number of request per second - // limited on the "new-reg", "new-authz" and "new-cert" endpoints. - // From the documentation the limitation is 20 requests per second, - // but using 20 as value doesn't work but 18 do. - // https://letsencrypt.org/docs/rate-limits/ - // ZeroSSL has a limit of 7. - // https://help.zerossl.com/hc/en-us/articles/17864245480093-Advantages-over-Using-Let-s-Encrypt#h_01HT4Z1JCJFJQFJ1M3P7S085Q9 - DefaultOverallRequestLimit = 18 -) - // maxBodySize is the maximum size of body that we will read. const maxBodySize = 1024 * 1024 @@ -65,26 +54,18 @@ type Resource struct { // If `AlwaysDeactivateAuthorizations` is true, the authorizations are also relinquished if the obtain request was successful. // See https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.2. type ObtainRequest struct { - Domains []string - PrivateKey crypto.PrivateKey - MustStaple bool - EmailAddresses []string - - NotBefore time.Time - NotAfter time.Time - Bundle bool - PreferredChain string - - // A string uniquely identifying the profile - // which will be used to affect issuance of the certificate requested by this Order. - // - https://www.ietf.org/id/draft-ietf-acme-profiles-00.html#section-4 - Profile string + Domains []string + PrivateKey crypto.PrivateKey + MustStaple bool + NotBefore time.Time + NotAfter time.Time + Bundle bool + PreferredChain string AlwaysDeactivateAuthorizations bool - // A string uniquely identifying a previously-issued certificate which this // order is intended to replace. - // - https://www.rfc-editor.org/rfc/rfc9773.html#section-5 + // - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5 ReplacesCertID string } @@ -97,23 +78,14 @@ type ObtainRequest struct { type ObtainForCSRRequest struct { CSR *x509.CertificateRequest - PrivateKey crypto.PrivateKey - - NotBefore time.Time - NotAfter time.Time - Bundle bool - PreferredChain string - - // A string uniquely identifying the profile - // which will be used to affect issuance of the certificate requested by this Order. - // - https://www.ietf.org/id/draft-ietf-acme-profiles-00.html#section-4 - Profile string - + NotBefore time.Time + NotAfter time.Time + Bundle bool + PreferredChain string AlwaysDeactivateAuthorizations bool - // A string uniquely identifying a previously-issued certificate which this // order is intended to replace. - // - https://www.rfc-editor.org/rfc/rfc9773.html#section-5 + // - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5 ReplacesCertID string } @@ -122,34 +94,24 @@ type resolver interface { } type CertifierOptions struct { - KeyType certcrypto.KeyType - Timeout time.Duration - OverallRequestLimit int - DisableCommonName bool + KeyType certcrypto.KeyType + Timeout time.Duration } // Certifier A service to obtain/renew/revoke certificates. type Certifier struct { - core *api.Core - resolver resolver - options CertifierOptions - overallRequestLimit int + core *api.Core + resolver resolver + options CertifierOptions } // NewCertifier creates a Certifier. func NewCertifier(core *api.Core, resolver resolver, options CertifierOptions) *Certifier { - c := &Certifier{ + return &Certifier{ core: core, resolver: resolver, options: options, } - - c.overallRequestLimit = options.OverallRequestLimit - if c.overallRequestLimit <= 0 { - c.overallRequestLimit = DefaultOverallRequestLimit - } - - return c } // Obtain tries to obtain a single certificate using all domains passed into it. @@ -172,7 +134,6 @@ func (c *Certifier) Obtain(request ObtainRequest) (*Resource, error) { orderOpts := &api.OrderOptions{ NotBefore: request.NotBefore, NotAfter: request.NotAfter, - Profile: request.Profile, ReplacesCertID: request.ReplacesCertID, } @@ -198,8 +159,7 @@ func (c *Certifier) Obtain(request ObtainRequest) (*Resource, error) { log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) failures := newObtainError() - - cert, err := c.getForOrder(domains, order, request) + cert, err := c.getForOrder(domains, order, request.Bundle, request.PrivateKey, request.MustStaple, request.PreferredChain) if err != nil { for _, auth := range authz { failures.Add(challenge.GetTargetedDomain(auth), err) @@ -240,7 +200,6 @@ func (c *Certifier) ObtainForCSR(request ObtainForCSRRequest) (*Resource, error) orderOpts := &api.OrderOptions{ NotBefore: request.NotBefore, NotAfter: request.NotAfter, - Profile: request.Profile, ReplacesCertID: request.ReplacesCertID, } @@ -266,13 +225,7 @@ func (c *Certifier) ObtainForCSR(request ObtainForCSRRequest) (*Resource, error) log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) failures := newObtainError() - - var privateKey []byte - if request.PrivateKey != nil { - privateKey = certcrypto.PEMEncode(request.PrivateKey) - } - - cert, err := c.getForCSR(domains, order, request.Bundle, request.CSR.Raw, privateKey, request.PreferredChain) + cert, err := c.getForCSR(domains, order, request.Bundle, request.CSR.Raw, nil, request.PreferredChain) if err != nil { for _, auth := range authz { failures.Add(challenge.GetTargetedDomain(auth), err) @@ -291,12 +244,9 @@ func (c *Certifier) ObtainForCSR(request ObtainForCSRRequest) (*Resource, error) return cert, failures.Join() } -func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, request ObtainRequest) (*Resource, error) { - privateKey := request.PrivateKey - +func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, bundle bool, privateKey crypto.PrivateKey, mustStaple bool, preferredChain string) (*Resource, error) { if privateKey == nil { var err error - privateKey, err = certcrypto.GeneratePrivateKey(c.options.KeyType) if err != nil { return nil, err @@ -304,7 +254,7 @@ func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, requ } commonName := "" - if len(domains[0]) <= 64 && !c.options.DisableCommonName { + if len(domains[0]) <= 64 { commonName = domains[0] } @@ -326,19 +276,13 @@ func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, requ } } - csrOptions := certcrypto.CSROptions{ - Domain: commonName, - SAN: san, - MustStaple: request.MustStaple, - EmailAddresses: request.EmailAddresses, - } - - csr, err := certcrypto.CreateCSR(privateKey, csrOptions) + // TODO: should the CSR be customizable? + csr, err := certcrypto.GenerateCSR(privateKey, commonName, san, mustStaple) if err != nil { return nil, err } - return c.getForCSR(domains, order, request.Bundle, csr, certcrypto.PEMEncode(privateKey), request.PreferredChain) + return c.getForCSR(domains, order, bundle, csr, certcrypto.PEMEncode(privateKey), preferredChain) } func (c *Certifier) getForCSR(domains []string, order acme.ExtendedOrder, bundle bool, csr, privateKeyPem []byte, preferredChain string) (*Resource, error) { @@ -471,15 +415,11 @@ type RenewOptions struct { NotBefore time.Time NotAfter time.Time // If true, the []byte contains both the issuer certificate and your issued certificate as a bundle. - Bundle bool - PreferredChain string - - Profile string - + Bundle bool + PreferredChain string AlwaysDeactivateAuthorizations bool // Not supported for CSR request. - MustStaple bool - EmailAddresses []string + MustStaple bool } // Renew takes a Resource and tries to renew the certificate. @@ -492,7 +432,6 @@ type RenewOptions struct { // If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle. // // For private key reuse the PrivateKey property of the passed in Resource should be non-nil. -// // Deprecated: use RenewWithOptions instead. func (c *Certifier) Renew(certRes Resource, bundle, mustStaple bool, preferredChain string) (*Resource, error) { return c.RenewWithOptions(certRes, &RenewOptions{ @@ -546,7 +485,6 @@ func (c *Certifier) RenewWithOptions(certRes Resource, options *RenewOptions) (* request.NotAfter = options.NotAfter request.Bundle = options.Bundle request.PreferredChain = options.PreferredChain - request.Profile = options.Profile request.AlwaysDeactivateAuthorizations = options.AlwaysDeactivateAuthorizations } @@ -572,8 +510,6 @@ func (c *Certifier) RenewWithOptions(certRes Resource, options *RenewOptions) (* request.NotAfter = options.NotAfter request.Bundle = options.Bundle request.PreferredChain = options.PreferredChain - request.EmailAddresses = options.EmailAddresses - request.Profile = options.Profile request.AlwaysDeactivateAuthorizations = options.AlwaysDeactivateAuthorizations } @@ -712,7 +648,7 @@ func checkOrderStatus(order acme.ExtendedOrder) (bool, error) { case acme.StatusValid: return true, nil case acme.StatusInvalid: - return false, fmt.Errorf("invalid order: %w", order.Err()) + return false, order.Error default: return false, nil } @@ -725,7 +661,6 @@ func checkOrderStatus(order acme.ExtendedOrder) (bool, error) { // https://www.rfc-editor.org/rfc/rfc5280.html#section-7 func sanitizeDomain(domains []string) []string { var sanitizedDomains []string - for _, domain := range domains { sanitizedDomain, err := idna.ToASCII(domain) if err != nil { @@ -734,6 +669,5 @@ func sanitizeDomain(domains []string) []string { sanitizedDomains = append(sanitizedDomains, sanitizedDomain) } } - return sanitizedDomains } diff --git a/certificate/certificates_test.go b/certificate/certificates_test.go index c0e35e795..bff66429d 100644 --- a/certificate/certificates_test.go +++ b/certificate/certificates_test.go @@ -3,6 +3,7 @@ package certificate import ( "crypto/rand" "crypto/rsa" + "encoding/pem" "fmt" "net/http" "testing" @@ -11,7 +12,6 @@ import ( "github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -175,14 +175,20 @@ Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ ` func Test_checkResponse(t *testing.T) { - server := tester.MockACMEServer(). - Route("POST /certificate", servermock.RawStringResponse(certResponseMock)). - BuildHTTPS(t) + mux, apiURL := tester.SetupFakeAPI(t) + + mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) { + _, err := w.Write([]byte(certResponseMock)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") - core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", key) + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) require.NoError(t, err) certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048}) @@ -190,7 +196,7 @@ func Test_checkResponse(t *testing.T) { order := acme.ExtendedOrder{ Order: acme.Order{ Status: acme.StatusValid, - Certificate: server.URL + "/certificate", + Certificate: apiURL + "/certificate", }, } certRes := &Resource{} @@ -199,7 +205,7 @@ func Test_checkResponse(t *testing.T) { require.NoError(t, err) assert.True(t, valid) assert.NotNil(t, certRes) - assert.Empty(t, certRes.Domain) + assert.Equal(t, "", certRes.Domain) assert.Contains(t, certRes.CertStableURL, "/certificate") assert.Contains(t, certRes.CertURL, "/certificate") assert.Nil(t, certRes.CSR) @@ -209,14 +215,30 @@ func Test_checkResponse(t *testing.T) { } func Test_checkResponse_issuerRelUp(t *testing.T) { - server := tester.MockACMEServer(). - Route("POST /certificate", servermock.RawStringResponse(certResponseMock)). - BuildHTTPS(t) + mux, apiURL := tester.SetupFakeAPI(t) + + mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Link", "<"+apiURL+`/issuer>; rel="up"`) + _, err := w.Write([]byte(certResponseMock)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + mux.HandleFunc("/issuer", func(w http.ResponseWriter, _ *http.Request) { + p, _ := pem.Decode([]byte(issuerMock)) + _, err := w.Write(p.Bytes) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") - core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", key) + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) require.NoError(t, err) certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048}) @@ -224,7 +246,7 @@ func Test_checkResponse_issuerRelUp(t *testing.T) { order := acme.ExtendedOrder{ Order: acme.Order{ Status: acme.StatusValid, - Certificate: server.URL + "/certificate", + Certificate: apiURL + "/certificate", }, } certRes := &Resource{} @@ -233,7 +255,7 @@ func Test_checkResponse_issuerRelUp(t *testing.T) { require.NoError(t, err) assert.True(t, valid) assert.NotNil(t, certRes) - assert.Empty(t, certRes.Domain) + assert.Equal(t, "", certRes.Domain) assert.Contains(t, certRes.CertStableURL, "/certificate") assert.Contains(t, certRes.CertURL, "/certificate") assert.Nil(t, certRes.CSR) @@ -243,14 +265,20 @@ func Test_checkResponse_issuerRelUp(t *testing.T) { } func Test_checkResponse_no_bundle(t *testing.T) { - server := tester.MockACMEServer(). - Route("POST /certificate", servermock.RawStringResponse(certResponseMock)). - BuildHTTPS(t) + mux, apiURL := tester.SetupFakeAPI(t) + + mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) { + _, err := w.Write([]byte(certResponseMock)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") - core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", key) + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) require.NoError(t, err) certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048}) @@ -258,7 +286,7 @@ func Test_checkResponse_no_bundle(t *testing.T) { order := acme.ExtendedOrder{ Order: acme.Order{ Status: acme.StatusValid, - Certificate: server.URL + "/certificate", + Certificate: apiURL + "/certificate", }, } certRes := &Resource{} @@ -267,7 +295,7 @@ func Test_checkResponse_no_bundle(t *testing.T) { require.NoError(t, err) assert.True(t, valid) assert.NotNil(t, certRes) - assert.Empty(t, certRes.Domain) + assert.Equal(t, "", certRes.Domain) assert.Contains(t, certRes.CertStableURL, "/certificate") assert.Contains(t, certRes.CertURL, "/certificate") assert.Nil(t, certRes.CSR) @@ -277,21 +305,30 @@ func Test_checkResponse_no_bundle(t *testing.T) { } func Test_checkResponse_alternate(t *testing.T) { - server := tester.MockACMEServer(). - Route("POST /certificate", - http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.Header().Add("Link", - fmt.Sprintf(`;title="foo";rel="alternate"`, req.Context().Value(http.LocalAddrContextKey))) + mux, apiURL := tester.SetupFakeAPI(t) - servermock.RawStringResponse(certResponseMock).ServeHTTP(rw, req) - })). - Route("/certificate/1", servermock.RawStringResponse(certResponseMock2)). - BuildHTTPS(t) + mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Add("Link", fmt.Sprintf(`<%s/certificate/1>;title="foo";rel="alternate"`, apiURL)) + + _, err := w.Write([]byte(certResponseMock)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + mux.HandleFunc("/certificate/1", func(w http.ResponseWriter, _ *http.Request) { + _, err := w.Write([]byte(certResponseMock2)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") - core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", key) + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) require.NoError(t, err) certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048}) @@ -299,7 +336,7 @@ func Test_checkResponse_alternate(t *testing.T) { order := acme.ExtendedOrder{ Order: acme.Order{ Status: acme.StatusValid, - Certificate: server.URL + "/certificate", + Certificate: apiURL + "/certificate", }, } certRes := &Resource{ @@ -321,76 +358,37 @@ func Test_checkResponse_alternate(t *testing.T) { } func Test_Get(t *testing.T) { - server := tester.MockACMEServer(). - Route("POST /acme/cert/test-cert", servermock.RawStringResponse(certResponseMock)). - BuildHTTPS(t) + mux, apiURL := tester.SetupFakeAPI(t) + + mux.HandleFunc("/acme/cert/test-cert", func(w http.ResponseWriter, _ *http.Request) { + _, err := w.Write([]byte(certResponseMock)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") - core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", key) + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) require.NoError(t, err) certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048}) - certRes, err := certifier.Get(server.URL+"/acme/cert/test-cert", true) + certRes, err := certifier.Get(apiURL+"/acme/cert/test-cert", true) require.NoError(t, err) assert.NotNil(t, certRes) assert.Equal(t, "acme.wtf", certRes.Domain) - assert.Equal(t, server.URL+"/acme/cert/test-cert", certRes.CertStableURL) - assert.Equal(t, server.URL+"/acme/cert/test-cert", certRes.CertURL) + assert.Equal(t, apiURL+"/acme/cert/test-cert", certRes.CertStableURL) + assert.Equal(t, apiURL+"/acme/cert/test-cert", certRes.CertURL) assert.Nil(t, certRes.CSR) assert.Nil(t, certRes.PrivateKey) assert.Equal(t, certResponseMock, string(certRes.Certificate), "Certificate") assert.Equal(t, issuerMock, string(certRes.IssuerCertificate), "IssuerCertificate") } -func Test_checkOrderStatus(t *testing.T) { - testCases := []struct { - desc string - order acme.Order - requireErr require.ErrorAssertionFunc - expected bool - }{ - { - desc: "status valid", - order: acme.Order{Status: acme.StatusValid}, - requireErr: require.NoError, - expected: true, - }, - { - desc: "status invalid", - order: acme.Order{Status: acme.StatusInvalid}, - requireErr: require.Error, - expected: false, - }, - { - desc: "status invalid with error", - order: acme.Order{Status: acme.StatusInvalid, Error: &acme.ProblemDetails{}}, - requireErr: require.Error, - expected: false, - }, - { - desc: "unknown status", - order: acme.Order{Status: "foo"}, - requireErr: require.NoError, - expected: false, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - status, err := checkOrderStatus(acme.ExtendedOrder{Order: test.order}) - test.requireErr(t, err) - - assert.Equal(t, test.expected, status) - }) - } -} - type resolverMock struct { error error } diff --git a/certificate/renewal.go b/certificate/renewal.go index 59d31cfb5..66c93acba 100644 --- a/certificate/renewal.go +++ b/certificate/renewal.go @@ -11,7 +11,6 @@ import ( "time" "github.com/go-acme/lego/v4/acme" - "github.com/go-acme/lego/v4/acme/api" ) // RenewalInfoRequest contains the necessary renewal information. @@ -26,15 +25,15 @@ type RenewalInfoResponse struct { // RetryAfter header indicating the polling interval that the ACME server recommends. // Conforming clients SHOULD query the renewalInfo URL again after the RetryAfter period has passed, // as the server may provide a different suggestedWindow. - // https://www.rfc-editor.org/rfc/rfc9773.html#section-4.2 + // https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-4.2 RetryAfter time.Duration } // ShouldRenewAt determines the optimal renewal time based on the current time (UTC),renewal window suggest by ARI, and the client's willingness to sleep. // It returns a pointer to a time.Time value indicating when the renewal should be attempted or nil if deferred until the next normal wake time. -// This method implements the RECOMMENDED algorithm described in RFC 9773. +// This method implements the RECOMMENDED algorithm described in draft-ietf-acme-ari. // -// - (4.1-11. Getting Renewal Information) https://www.rfc-editor.org/rfc/rfc9773.html +// - (4.1-11. Getting Renewal Information) https://datatracker.ietf.org/doc/draft-ietf-acme-ari/ func (r *RenewalInfoResponse) ShouldRenewAt(now time.Time, willingToSleep time.Duration) *time.Time { // Explicitly convert all times to UTC. now = now.UTC() @@ -42,11 +41,9 @@ func (r *RenewalInfoResponse) ShouldRenewAt(now time.Time, willingToSleep time.D end := r.SuggestedWindow.End.UTC() // Select a uniform random time within the suggested window. - rt := start - if window := end.Sub(start); window > 0 { - randomDuration := time.Duration(rand.Int63n(int64(window))) - rt = rt.Add(randomDuration) - } + window := end.Sub(start) + randomDuration := time.Duration(rand.Int63n(int64(window))) + rt := start.Add(randomDuration) // If the selected time is in the past, attempt renewal immediately. if rt.Before(now) { @@ -72,7 +69,7 @@ func (r *RenewalInfoResponse) ShouldRenewAt(now time.Time, willingToSleep time.D // Note: this endpoint is part of a draft specification, not all ACME servers will implement it. // This method will return api.ErrNoARI if the server does not advertise a renewal info endpoint. // -// https://www.rfc-editor.org/rfc/rfc9773.html +// https://datatracker.ietf.org/doc/draft-ietf-acme-ari func (c *Certifier) GetRenewalInfo(req RenewalInfoRequest) (*RenewalInfoResponse, error) { certID, err := MakeARICertID(req.Cert) if err != nil { @@ -86,23 +83,22 @@ func (c *Certifier) GetRenewalInfo(req RenewalInfoRequest) (*RenewalInfoResponse defer resp.Body.Close() var info RenewalInfoResponse - err = json.NewDecoder(resp.Body).Decode(&info) if err != nil { return nil, err } if retry := resp.Header.Get("Retry-After"); retry != "" { - info.RetryAfter, err = api.ParseRetryAfter(retry) + info.RetryAfter, err = time.ParseDuration(retry + "s") if err != nil { - return nil, fmt.Errorf("failed to parse Retry-After header: %w", err) + return nil, err } } return &info, nil } -// MakeARICertID constructs a certificate identifier as described in RFC 9773, section 4.1. +// MakeARICertID constructs a certificate identifier as described in draft-ietf-acme-ari-03, section 4.1. func MakeARICertID(leaf *x509.Certificate) (string, error) { if leaf == nil { return "", errors.New("leaf certificate is nil") diff --git a/certificate/renewal_test.go b/certificate/renewal_test.go index 23209638a..9f20e374e 100644 --- a/certificate/renewal_test.go +++ b/certificate/renewal_test.go @@ -11,7 +11,6 @@ import ( "github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -43,24 +42,31 @@ func TestCertifier_GetRenewalInfo(t *testing.T) { require.NoError(t, err) // Test with a fake API. - server := tester.MockACMEServer(). - Route("GET /renewalInfo/"+ariLeafCertID, - servermock.RawStringResponse(`{ + mux, apiURL := tester.SetupFakeAPI(t) + mux.HandleFunc("/renewalInfo/"+ariLeafCertID, func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Retry-After", "21600") + w.WriteHeader(http.StatusOK) + _, wErr := w.Write([]byte(`{ "suggestedWindow": { "start": "2020-03-17T17:51:09Z", "end": "2020-03-17T18:21:09Z" }, - "explanationUrl": "https://aricapable.ca.example/docs/renewal-advice/" + "explanationUrl": "https://aricapable.ca/docs/renewal-advice/" } - }`). - WithHeader("Content-Type", "application/json"). - WithHeader("Retry-After", "21600")). - BuildHTTPS(t) + }`)) + require.NoError(t, wErr) + }) key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") - core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", key) + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) require.NoError(t, err) certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048}) @@ -70,46 +76,10 @@ func TestCertifier_GetRenewalInfo(t *testing.T) { require.NotNil(t, ri) assert.Equal(t, "2020-03-17T17:51:09Z", ri.SuggestedWindow.Start.Format(time.RFC3339)) assert.Equal(t, "2020-03-17T18:21:09Z", ri.SuggestedWindow.End.Format(time.RFC3339)) - assert.Equal(t, "https://aricapable.ca.example/docs/renewal-advice/", ri.ExplanationURL) + assert.Equal(t, "https://aricapable.ca/docs/renewal-advice/", ri.ExplanationURL) assert.Equal(t, time.Duration(21600000000000), ri.RetryAfter) } -func TestCertifier_GetRenewalInfo_retryAfter(t *testing.T) { - leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM)) - require.NoError(t, err) - - server := tester.MockACMEServer(). - Route("GET /renewalInfo/"+ariLeafCertID, - servermock.RawStringResponse(`{ - "suggestedWindow": { - "start": "2020-03-17T17:51:09Z", - "end": "2020-03-17T18:21:09Z" - }, - "explanationUrl": "https://aricapable.ca.example/docs/renewal-advice/" - } - }`). - WithHeader("Content-Type", "application/json"). - WithHeader("Retry-After", time.Now().UTC().Add(6*time.Hour).Format(time.RFC1123))). - BuildHTTPS(t) - - key, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err, "Could not generate test key") - - core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", key) - require.NoError(t, err) - - certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048}) - - ri, err := certifier.GetRenewalInfo(RenewalInfoRequest{leaf}) - require.NoError(t, err) - require.NotNil(t, ri) - assert.Equal(t, "2020-03-17T17:51:09Z", ri.SuggestedWindow.Start.Format(time.RFC3339)) - assert.Equal(t, "2020-03-17T18:21:09Z", ri.SuggestedWindow.End.Format(time.RFC3339)) - assert.Equal(t, "https://aricapable.ca.example/docs/renewal-advice/", ri.ExplanationURL) - - assert.InDelta(t, 6, ri.RetryAfter.Hours(), 0.001) -} - func TestCertifier_GetRenewalInfo_errors(t *testing.T) { leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM)) require.NoError(t, err) @@ -118,23 +88,24 @@ func TestCertifier_GetRenewalInfo_errors(t *testing.T) { require.NoError(t, err, "Could not generate test key") testCases := []struct { - desc string - timeout time.Duration - request RenewalInfoRequest - handler http.HandlerFunc + desc string + httpClient *http.Client + request RenewalInfoRequest + handler http.HandlerFunc }{ { - desc: "API timeout", - timeout: 500 * time.Millisecond, // HTTP client that times out after 500ms. - request: RenewalInfoRequest{leaf}, + desc: "API timeout", + httpClient: &http.Client{Timeout: 500 * time.Millisecond}, // HTTP client that times out after 500ms. + request: RenewalInfoRequest{leaf}, handler: func(w http.ResponseWriter, r *http.Request) { // API that takes 2ms to respond. time.Sleep(2 * time.Millisecond) }, }, { - desc: "API error", - request: RenewalInfoRequest{leaf}, + desc: "API error", + httpClient: http.DefaultClient, + request: RenewalInfoRequest{leaf}, handler: func(w http.ResponseWriter, r *http.Request) { // API that responds with error instead of renewal info. http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) @@ -146,17 +117,10 @@ func TestCertifier_GetRenewalInfo_errors(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - server := tester.MockACMEServer(). - Route("GET /renewalInfo/"+ariLeafCertID, test.handler). - BuildHTTPS(t) + mux, apiURL := tester.SetupFakeAPI(t) + mux.HandleFunc("/renewalInfo/"+ariLeafCertID, test.handler) - client := server.Client() - - if test.timeout != 0 { - client.Timeout = test.timeout - } - - core, err := api.New(client, "lego-test", server.URL+"/dir", "", key) + core, err := api.New(test.httpClient, "lego-test", apiURL+"/dir", "", key) require.NoError(t, err) certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048}) diff --git a/challenge/challenges.go b/challenge/challenges.go index f6d5cdb28..39bf3bee2 100644 --- a/challenge/challenges.go +++ b/challenge/challenges.go @@ -40,6 +40,5 @@ func GetTargetedDomain(authz acme.Authorization) string { if authz.Wildcard { return "*." + authz.Identifier.Value } - return authz.Identifier.Value } diff --git a/challenge/dns01/dns_challenge.go b/challenge/dns01/dns_challenge.go index 1d106d7b7..8594d2799 100644 --- a/challenge/dns01/dns_challenge.go +++ b/challenge/dns01/dns_challenge.go @@ -40,7 +40,6 @@ func CondOption(condition bool, opt ChallengeOption) ChallengeOption { return nil } } - return opt } @@ -119,7 +118,6 @@ func (c *Challenge) Solve(authz acme.Authorization) error { info := GetChallengeInfo(authz.Identifier.Value, keyAuth) var timeout, interval time.Duration - switch provider := c.provider.(type) { case challenge.ProviderTimeout: timeout, interval = provider.Timeout() @@ -136,7 +134,6 @@ func (c *Challenge) Solve(authz acme.Authorization) error { if !stop || errP != nil { log.Infof("[%s] acme: Waiting for DNS record propagation.", domain) } - return stop, errP }) if err != nil { @@ -144,7 +141,6 @@ func (c *Challenge) Solve(authz acme.Authorization) error { } chlng.KeyAuthorization = keyAuth - return c.validate(c.core, domain, chlng) } @@ -169,7 +165,6 @@ func (c *Challenge) Sequential() (bool, time.Duration) { if p, ok := c.provider.(sequential); ok { return ok, p.Sequential() } - return false, 0 } @@ -178,7 +173,6 @@ type sequential interface { } // GetRecord returns a DNS record which will fulfill the `dns-01` challenge. -// // Deprecated: use GetChallengeInfo instead. func GetRecord(domain, keyAuth string) (fqdn, value string) { info := GetChallengeInfo(domain, keyAuth) diff --git a/challenge/dns01/dns_challenge_manual.go b/challenge/dns01/dns_challenge_manual.go index 3821fc157..c00d64041 100644 --- a/challenge/dns01/dns_challenge_manual.go +++ b/challenge/dns01/dns_challenge_manual.go @@ -12,14 +12,9 @@ const ( ) // DNSProviderManual is an implementation of the ChallengeProvider interface. -// TODO(ldez): move this to providers/dns/manual -// -// Deprecated: Use the manual.DNSProvider instead. type DNSProviderManual struct{} // NewDNSProviderManual returns a DNSProviderManual instance. -// -// Deprecated: Use the manual.NewDNSProvider instead. func NewDNSProviderManual() (*DNSProviderManual, error) { return &DNSProviderManual{}, nil } diff --git a/providers/dns/manual/manual_test.go b/challenge/dns01/dns_challenge_manual_test.go similarity index 85% rename from providers/dns/manual/manual_test.go rename to challenge/dns01/dns_challenge_manual_test.go index 7badd4b8b..cfc728aca 100644 --- a/providers/dns/manual/manual_test.go +++ b/challenge/dns01/dns_challenge_manual_test.go @@ -1,4 +1,4 @@ -package manual +package dns01 import ( "io" @@ -10,7 +10,6 @@ import ( func TestDNSProviderManual(t *testing.T) { backupStdin := os.Stdin - defer func() { os.Stdin = backupStdin }() testCases := []struct { @@ -31,10 +30,9 @@ func TestDNSProviderManual(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - file, err := os.CreateTemp(t.TempDir(), "lego_test") + file, err := os.CreateTemp("", "lego_test") require.NoError(t, err) - - t.Cleanup(func() { _ = file.Close() }) + defer func() { _ = os.Remove(file.Name()) }() _, err = file.WriteString(test.input) require.NoError(t, err) @@ -44,7 +42,7 @@ func TestDNSProviderManual(t *testing.T) { os.Stdin = file - manualProvider, err := NewDNSProvider() + manualProvider, err := NewDNSProviderManual() require.NoError(t, err) err = manualProvider.Present("example.com", "", "") diff --git a/challenge/dns01/dns_challenge_test.go b/challenge/dns01/dns_challenge_test.go index 325f1656c..953180326 100644 --- a/challenge/dns01/dns_challenge_test.go +++ b/challenge/dns01/dns_challenge_test.go @@ -4,6 +4,7 @@ import ( "crypto/rand" "crypto/rsa" "errors" + "net/http" "testing" "time" @@ -11,8 +12,6 @@ import ( "github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/dnsmock" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -33,12 +32,12 @@ func (p *providerTimeoutMock) CleanUp(domain, token, keyAuth string) error { ret func (p *providerTimeoutMock) Timeout() (time.Duration, time.Duration) { return p.timeout, p.interval } func TestChallenge_PreSolve(t *testing.T) { - server := tester.MockACMEServer().BuildHTTPS(t) + _, apiURL := tester.SetupFakeAPI(t) - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) + privateKey, err := rsa.GenerateKey(rand.Reader, 512) require.NoError(t, err) - core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey) + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) testCases := []struct { @@ -115,16 +114,12 @@ func TestChallenge_PreSolve(t *testing.T) { } func TestChallenge_Solve(t *testing.T) { - useAsNameserver(t, dnsmock.NewServer(). - Query("_acme-challenge.example.com. CNAME", dnsmock.Noop). - Build(t)) + _, apiURL := tester.SetupFakeAPI(t) - server := tester.MockACMEServer().BuildHTTPS(t) - - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) + privateKey, err := rsa.GenerateKey(rand.Reader, 512) require.NoError(t, err) - core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey) + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) testCases := []struct { @@ -184,7 +179,6 @@ func TestChallenge_Solve(t *testing.T) { if test.preCheck != nil { options = append(options, WrapPreCheck(test.preCheck)) } - chlg := NewChallenge(core, test.validate, test.provider, options...) authz := acme.Authorization{ @@ -207,12 +201,12 @@ func TestChallenge_Solve(t *testing.T) { } func TestChallenge_CleanUp(t *testing.T) { - server := tester.MockACMEServer().BuildHTTPS(t) + _, apiURL := tester.SetupFakeAPI(t) - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) + privateKey, err := rsa.GenerateKey(rand.Reader, 512) require.NoError(t, err) - core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey) + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) testCases := []struct { @@ -287,55 +281,3 @@ func TestChallenge_CleanUp(t *testing.T) { }) } } - -func TestGetChallengeInfo(t *testing.T) { - useAsNameserver(t, dnsmock.NewServer(). - Query("_acme-challenge.example.com. CNAME", dnsmock.Noop). - Build(t)) - - info := GetChallengeInfo("example.com", "123") - - expected := ChallengeInfo{ - FQDN: "_acme-challenge.example.com.", - EffectiveFQDN: "_acme-challenge.example.com.", - Value: "pmWkWSBCL51Bfkhn79xPuKBKHz__H6B-mY6G9_eieuM", - } - - assert.Equal(t, expected, info) -} - -func TestGetChallengeInfo_CNAME(t *testing.T) { - useAsNameserver(t, dnsmock.NewServer(). - Query("_acme-challenge.example.com. CNAME", dnsmock.CNAME("example.org.")). - Query("example.org. CNAME", dnsmock.Noop). - Build(t)) - - info := GetChallengeInfo("example.com", "123") - - expected := ChallengeInfo{ - FQDN: "_acme-challenge.example.com.", - EffectiveFQDN: "example.org.", - Value: "pmWkWSBCL51Bfkhn79xPuKBKHz__H6B-mY6G9_eieuM", - } - - assert.Equal(t, expected, info) -} - -func TestGetChallengeInfo_CNAME_disabled(t *testing.T) { - useAsNameserver(t, dnsmock.NewServer(). - // Never called when the env var works. - Query("_acme-challenge.example.com. CNAME", dnsmock.CNAME("example.org.")). - Build(t)) - - t.Setenv("LEGO_DISABLE_CNAME_SUPPORT", "true") - - info := GetChallengeInfo("example.com", "123") - - expected := ChallengeInfo{ - FQDN: "_acme-challenge.example.com.", - EffectiveFQDN: "_acme-challenge.example.com.", - Value: "pmWkWSBCL51Bfkhn79xPuKBKHz__H6B-mY6G9_eieuM", - } - - assert.Equal(t, expected, info) -} diff --git a/challenge/dns01/fixtures/resolv.conf.1 b/challenge/dns01/fixtures/resolv.conf.1 index bc2a3c1ac..3098f99b5 100644 --- a/challenge/dns01/fixtures/resolv.conf.1 +++ b/challenge/dns01/fixtures/resolv.conf.1 @@ -1,4 +1,4 @@ -domain example.com +domain company.com nameserver 10.200.3.249 nameserver 10.200.3.250:5353 nameserver 2001:4860:4860::8844 diff --git a/challenge/dns01/fqdn.go b/challenge/dns01/fqdn.go index 11ac3d0c2..c238c8cf5 100644 --- a/challenge/dns01/fqdn.go +++ b/challenge/dns01/fqdn.go @@ -1,16 +1,12 @@ package dns01 -import ( - "iter" - - "github.com/miekg/dns" -) - // ToFqdn converts the name into a fqdn appending a trailing dot. -// -// Deprecated: Use [github.com/miekg/dns.Fqdn] directly. func ToFqdn(name string) string { - return dns.Fqdn(name) + n := len(name) + if n == 0 || name[n-1] == '.' { + return name + } + return name + "." } // UnFqdn converts the fqdn into a name removing the trailing dot. @@ -19,36 +15,5 @@ func UnFqdn(name string) string { if n != 0 && name[n-1] == '.' { return name[:n-1] } - return name } - -// UnFqdnDomainsSeq generates a sequence of "unFQDNed" domain names derived from a domain (FQDN or not) in descending order. -func UnFqdnDomainsSeq(fqdn string) iter.Seq[string] { - return func(yield func(string) bool) { - if fqdn == "" { - return - } - - for _, index := range dns.Split(fqdn) { - if !yield(UnFqdn(fqdn[index:])) { - return - } - } - } -} - -// DomainsSeq generates a sequence of domain names derived from a domain (FQDN or not) in descending order. -func DomainsSeq(fqdn string) iter.Seq[string] { - return func(yield func(string) bool) { - if fqdn == "" { - return - } - - for _, index := range dns.Split(fqdn) { - if !yield(fqdn[index:]) { - return - } - } - } -} diff --git a/challenge/dns01/fqdn_test.go b/challenge/dns01/fqdn_test.go index 641e39081..a902667a2 100644 --- a/challenge/dns01/fqdn_test.go +++ b/challenge/dns01/fqdn_test.go @@ -1,12 +1,39 @@ package dns01 import ( - "slices" "testing" "github.com/stretchr/testify/assert" ) +func TestToFqdn(t *testing.T) { + testCases := []struct { + desc string + domain string + expected string + }{ + { + desc: "simple", + domain: "foo.example.com", + expected: "foo.example.com.", + }, + { + desc: "already FQDN", + domain: "foo.example.com.", + expected: "foo.example.com.", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + fqdn := ToFqdn(test.domain) + assert.Equal(t, test.expected, fqdn) + }) + } +} + func TestUnFqdn(t *testing.T) { testCases := []struct { desc string @@ -35,103 +62,3 @@ func TestUnFqdn(t *testing.T) { }) } } - -func TestUnFqdnDomainsSeq(t *testing.T) { - testCases := []struct { - desc string - fqdn string - expected []string - }{ - { - desc: "empty", - fqdn: "", - expected: nil, - }, - { - desc: "TLD", - fqdn: "com", - expected: []string{"com"}, - }, - { - desc: "2 levels", - fqdn: "example.com", - expected: []string{"example.com", "com"}, - }, - { - desc: "3 levels", - fqdn: "foo.example.com", - expected: []string{"foo.example.com", "example.com", "com"}, - }, - } - - for _, test := range testCases { - for name, suffix := range map[string]string{"": "", " FQDN": "."} { //nolint:gocritic - t.Run(test.desc+name, func(t *testing.T) { - t.Parallel() - - actual := slices.Collect(UnFqdnDomainsSeq(test.fqdn + suffix)) - - assert.Equal(t, test.expected, actual) - }) - } - } -} - -func TestDomainsSeq(t *testing.T) { - testCases := []struct { - desc string - fqdn string - expected []string - }{ - { - desc: "empty", - fqdn: "", - expected: nil, - }, - { - desc: "empty FQDN", - fqdn: ".", - expected: nil, - }, - { - desc: "TLD FQDN", - fqdn: "com", - expected: []string{"com"}, - }, - { - desc: "TLD", - fqdn: "com.", - expected: []string{"com."}, - }, - { - desc: "2 levels", - fqdn: "example.com", - expected: []string{"example.com", "com"}, - }, - { - desc: "2 levels FQDN", - fqdn: "example.com.", - expected: []string{"example.com.", "com."}, - }, - { - desc: "3 levels", - fqdn: "foo.example.com", - expected: []string{"foo.example.com", "example.com", "com"}, - }, - { - desc: "3 levels FQDN", - fqdn: "foo.example.com.", - expected: []string{"foo.example.com.", "example.com.", "com."}, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - actual := slices.Collect(DomainsSeq(test.fqdn)) - - assert.Equal(t, test.expected, actual) - }) - } -} diff --git a/challenge/dns01/mock_test.go b/challenge/dns01/mock_test.go deleted file mode 100644 index 5dcad3013..000000000 --- a/challenge/dns01/mock_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package dns01 - -import ( - "context" - "net" - "testing" - "time" - - "github.com/miekg/dns" - "github.com/stretchr/testify/require" -) - -func fakeNS(name, ns string) *dns.NS { - return &dns.NS{ - Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 172800}, - Ns: ns, - } -} - -func fakeA(name, ip string) *dns.A { - return &dns.A{ - Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 10}, - A: net.ParseIP(ip), - } -} - -func fakeTXT(name, value string) *dns.TXT { - return &dns.TXT{ - Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 10}, - Txt: []string{value}, - } -} - -// mockResolver modifies the default DNS resolver to use a custom network address during the test execution. -// IMPORTANT: it modifying global variables. -func mockResolver(t *testing.T, addr net.Addr) { - t.Helper() - - _, port, err := net.SplitHostPort(addr.String()) - require.NoError(t, err) - - originalDefaultNameserverPort := defaultNameserverPort - - t.Cleanup(func() { - defaultNameserverPort = originalDefaultNameserverPort - }) - - defaultNameserverPort = port - - originalResolver := net.DefaultResolver - - t.Cleanup(func() { - net.DefaultResolver = originalResolver - }) - - net.DefaultResolver = &net.Resolver{ - PreferGo: true, - Dial: func(ctx context.Context, network, address string) (net.Conn, error) { - d := net.Dialer{Timeout: 1 * time.Second} - - return d.DialContext(ctx, network, addr.String()) - }, - } -} - -func useAsNameserver(t *testing.T, addr net.Addr) { - t.Helper() - - ClearFqdnCache() - t.Cleanup(func() { - ClearFqdnCache() - }) - - originalRecursiveNameservers := recursiveNameservers - - t.Cleanup(func() { - recursiveNameservers = originalRecursiveNameservers - }) - - recursiveNameservers = ParseNameservers([]string{addr.String()}) -} diff --git a/challenge/dns01/nameserver.go b/challenge/dns01/nameserver.go index 554eb7cc2..206611be4 100644 --- a/challenge/dns01/nameserver.go +++ b/challenge/dns01/nameserver.go @@ -16,7 +16,10 @@ import ( const defaultResolvConf = "/etc/resolv.conf" -var fqdnSoaCache = &sync.Map{} +var ( + fqdnSoaCache = map[string]*soaCacheEntry{} + muFqdnSoaCache sync.Mutex +) var defaultNameservers = []string{ "google-public-dns-a.google.com:53", @@ -48,11 +51,9 @@ func (cache *soaCacheEntry) isExpired() bool { // ClearFqdnCache clears the cache of fqdn to zone mappings. Primarily used in testing. func ClearFqdnCache() { - // TODO(ldez): use `fqdnSoaCache.Clear()` when updating to go1.23 - fqdnSoaCache.Range(func(k, v any) bool { - fqdnSoaCache.Delete(k) - return true - }) + muFqdnSoaCache.Lock() + fqdnSoaCache = map[string]*soaCacheEntry{} + muFqdnSoaCache.Unlock() } func AddDNSTimeout(timeout time.Duration) ChallengeOption { @@ -81,7 +82,6 @@ func getNameservers(path string, defaults []string) []string { func ParseNameservers(servers []string) []string { var resolvers []string - for _, resolver := range servers { // ensure all servers have a port number if _, _, err := net.SplitHostPort(resolver); err != nil { @@ -90,7 +90,6 @@ func ParseNameservers(servers []string) []string { resolvers = append(resolvers, resolver) } } - return resolvers } @@ -134,7 +133,6 @@ func FindPrimaryNsByFqdnCustom(fqdn string, nameservers []string) (string, error if err != nil { return "", fmt.Errorf("[fqdn=%s] %w", fqdn, err) } - return soa.primaryNs, nil } @@ -151,18 +149,16 @@ func FindZoneByFqdnCustom(fqdn string, nameservers []string) (string, error) { if err != nil { return "", fmt.Errorf("[fqdn=%s] %w", fqdn, err) } - return soa.zone, nil } func lookupSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) { + muFqdnSoaCache.Lock() + defer muFqdnSoaCache.Unlock() + // Do we have it cached and is it still fresh? - entAny, ok := fqdnSoaCache.Load(fqdn) - if ok && entAny != nil { - ent, ok1 := entAny.(*soaCacheEntry) - if ok1 && !ent.isExpired() { - return ent, nil - } + if ent := fqdnSoaCache[fqdn]; ent != nil && !ent.isExpired() { + return ent, nil } ent, err := fetchSoaByFqdn(fqdn, nameservers) @@ -170,18 +166,18 @@ func lookupSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) return nil, err } - fqdnSoaCache.Store(fqdn, ent) - + fqdnSoaCache[fqdn] = ent return ent, nil } func fetchSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) { - var ( - err error - r *dns.Msg - ) + var err error + var r *dns.Msg + + labelIndexes := dns.Split(fqdn) + for _, index := range labelIndexes { + domain := fqdn[index:] - for domain := range DomainsSeq(fqdn) { r, err = dnsQuery(domain, dns.TypeSOA, nameservers, true) if err != nil { continue @@ -235,11 +231,9 @@ func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) ( return nil, &DNSError{Message: "empty list of nameservers"} } - var ( - r *dns.Msg - err error - errAll error - ) + var r *dns.Msg + var err error + var errAll error for _, ns := range nameservers { r, err = sendDNSQuery(m, ns) @@ -272,7 +266,6 @@ func createDNSMsg(fqdn string, rtype uint16, recursive bool) *dns.Msg { func sendDNSQuery(m *dns.Msg, ns string) (*dns.Msg, error) { if ok, _ := strconv.ParseBool(os.Getenv("LEGO_EXPERIMENTAL_DNS_TCP_ONLY")); ok { tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout} - r, _, err := tcp.Exchange(m, ns) if err != nil { return r, &DNSError{Message: "DNS call error", MsgIn: m, NS: ns, Err: err} diff --git a/challenge/dns01/nameserver_test.go b/challenge/dns01/nameserver_test.go index dd4d66dcb..15b19beba 100644 --- a/challenge/dns01/nameserver_test.go +++ b/challenge/dns01/nameserver_test.go @@ -5,237 +5,138 @@ import ( "sort" "testing" - "github.com/go-acme/lego/v4/platform/tester/dnsmock" "github.com/miekg/dns" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func Test_lookupNameserversOK(t *testing.T) { +func TestLookupNameserversOK(t *testing.T) { testCases := []struct { - desc string - fakeDNSServer *dnsmock.Builder - fqdn string - expected []string + fqdn string + nss []string }{ { - fqdn: "en.wikipedia.org.localhost.", - fakeDNSServer: dnsmock.NewServer(). - Query("en.wikipedia.org.localhost SOA", dnsmock.CNAME("dyna.wikimedia.org.localhost")). - Query("wikipedia.org.localhost SOA", dnsmock.SOA("")). - Query("wikipedia.org.localhost NS", - dnsmock.Answer( - fakeNS("wikipedia.org.localhost.", "ns0.wikimedia.org.localhost."), - fakeNS("wikipedia.org.localhost.", "ns1.wikimedia.org.localhost."), - fakeNS("wikipedia.org.localhost.", "ns2.wikimedia.org.localhost."), - ), - ), - expected: []string{"ns0.wikimedia.org.localhost.", "ns1.wikimedia.org.localhost.", "ns2.wikimedia.org.localhost."}, + fqdn: "en.wikipedia.org.", + nss: []string{"ns0.wikimedia.org.", "ns1.wikimedia.org.", "ns2.wikimedia.org."}, }, { - fqdn: "www.google.com.localhost.", - fakeDNSServer: dnsmock.NewServer(). - Query("www.google.com.localhost. SOA", dnsmock.Noop). - Query("google.com.localhost. SOA", dnsmock.SOA("")). - Query("google.com.localhost. NS", - dnsmock.Answer( - fakeNS("google.com.localhost.", "ns1.google.com.localhost."), - fakeNS("google.com.localhost.", "ns2.google.com.localhost."), - fakeNS("google.com.localhost.", "ns3.google.com.localhost."), - fakeNS("google.com.localhost.", "ns4.google.com.localhost."), - ), - ), - expected: []string{"ns1.google.com.localhost.", "ns2.google.com.localhost.", "ns3.google.com.localhost.", "ns4.google.com.localhost."}, + fqdn: "www.google.com.", + nss: []string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."}, }, { - fqdn: "mail.proton.me.localhost.", - fakeDNSServer: dnsmock.NewServer(). - Query("mail.proton.me.localhost. SOA", dnsmock.Noop). - Query("proton.me.localhost. SOA", dnsmock.SOA("")). - Query("proton.me.localhost. NS", - dnsmock.Answer( - fakeNS("proton.me.localhost.", "ns1.proton.me.localhost."), - fakeNS("proton.me.localhost.", "ns2.proton.me.localhost."), - fakeNS("proton.me.localhost.", "ns3.proton.me.localhost."), - ), - ), - expected: []string{"ns1.proton.me.localhost.", "ns2.proton.me.localhost.", "ns3.proton.me.localhost."}, + fqdn: "physics.georgetown.edu.", + nss: []string{"ns4.georgetown.edu.", "ns5.georgetown.edu.", "ns6.georgetown.edu."}, }, } for _, test := range testCases { t.Run(test.fqdn, func(t *testing.T) { - useAsNameserver(t, test.fakeDNSServer.Build(t)) + t.Parallel() nss, err := lookupNameservers(test.fqdn) require.NoError(t, err) sort.Strings(nss) - sort.Strings(test.expected) + sort.Strings(test.nss) - assert.Equal(t, test.expected, nss) + assert.EqualValues(t, test.nss, nss) }) } } -func Test_lookupNameserversErr(t *testing.T) { +func TestLookupNameserversErr(t *testing.T) { testCases := []struct { - desc string - fqdn string - fakeDNSServer *dnsmock.Builder - error string + desc string + fqdn string + error string }{ { - desc: "NXDOMAIN", - fqdn: "example.invalid.", - fakeDNSServer: dnsmock.NewServer(). - Query(". SOA", dnsmock.Error(dns.RcodeNameError)), - error: "could not find zone: [fqdn=example.invalid.] could not find the start of authority for 'example.invalid.' [question='invalid. IN SOA', code=NXDOMAIN]", - }, - { - desc: "NS error", - fqdn: "example.com.", - fakeDNSServer: dnsmock.NewServer(). - Query("example.com. SOA", dnsmock.SOA("")). - Query("example.com. NS", dnsmock.Error(dns.RcodeServerFailure)), - error: "[zone=example.com.] could not determine authoritative nameservers", - }, - { - desc: "empty NS", - fqdn: "example.com.", - fakeDNSServer: dnsmock.NewServer(). - Query("example.com. SOA", dnsmock.SOA("")). - Query("example.me NS", dnsmock.Noop), - error: "[zone=example.com.] could not determine authoritative nameservers", + desc: "invalid tld", + fqdn: "_null.n0n0.", + error: "could not find zone", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - useAsNameserver(t, test.fakeDNSServer.Build(t)) + t.Parallel() _, err := lookupNameservers(test.fqdn) require.Error(t, err) - assert.EqualError(t, err, test.error) + assert.Contains(t, err.Error(), test.error) }) } } -type lookupSoaByFqdnTestCase struct { +var findXByFqdnTestCases = []struct { desc string fqdn string zone string primaryNs string nameservers []string expectedError string -} - -func lookupSoaByFqdnTestCases(t *testing.T) []lookupSoaByFqdnTestCase { - t.Helper() - - return []lookupSoaByFqdnTestCase{ - { - desc: "domain is a CNAME", - fqdn: "mail.example.com.", - zone: "example.com.", - primaryNs: "ns1.example.com.", - nameservers: []string{ - dnsmock.NewServer(). - Query("mail.example.com. SOA", dnsmock.CNAME("example.com.")). - Query("example.com. SOA", dnsmock.SOA("")). - Build(t). - String(), - }, - }, - { - desc: "domain is a non-existent subdomain", - fqdn: "foo.example.com.", - zone: "example.com.", - primaryNs: "ns1.example.com.", - nameservers: []string{ - dnsmock.NewServer(). - Query("foo.example.com. SOA", dnsmock.Error(dns.RcodeNameError)). - Query("example.com. SOA", dnsmock.SOA("")). - Build(t). - String(), - }, - }, - { - desc: "domain is a eTLD", - fqdn: "example.com.ac.", - zone: "ac.", - primaryNs: "ns1.nic.ac.", - nameservers: []string{ - dnsmock.NewServer(). - Query("example.com.ac. SOA", dnsmock.Error(dns.RcodeNameError)). - Query("com.ac. SOA", dnsmock.Error(dns.RcodeNameError)). - Query("ac. SOA", dnsmock.SOA("")). - Build(t). - String(), - }, - }, - { - desc: "domain is a cross-zone CNAME", - fqdn: "cross-zone-example.example.com.", - zone: "example.com.", - primaryNs: "ns1.example.com.", - nameservers: []string{ - dnsmock.NewServer(). - Query("cross-zone-example.example.com. SOA", dnsmock.CNAME("example.org.")). - Query("example.com. SOA", dnsmock.SOA("")). - Build(t). - String(), - }, - }, - { - desc: "NXDOMAIN", - fqdn: "test.lego.invalid.", - zone: "lego.invalid.", - nameservers: []string{ - dnsmock.NewServer(). - Query("test.lego.invalid. SOA", dnsmock.Error(dns.RcodeNameError)). - Query("lego.invalid. SOA", dnsmock.Error(dns.RcodeNameError)). - Query("invalid. SOA", dnsmock.Error(dns.RcodeNameError)). - Build(t). - String(), - }, - expectedError: `[fqdn=test.lego.invalid.] could not find the start of authority for 'test.lego.invalid.' [question='invalid. IN SOA', code=NXDOMAIN]`, - }, - { - desc: "several non existent nameservers", - fqdn: "mail.example.com.", - zone: "example.com.", - primaryNs: "ns1.example.com.", - nameservers: []string{ - ":7053", - ":8053", - dnsmock.NewServer(). - Query("mail.example.com. SOA", dnsmock.CNAME("example.com.")). - Query("example.com. SOA", dnsmock.SOA("")). - Build(t). - String(), - }, - }, - { - desc: "only non-existent nameservers", - fqdn: "mail.example.com.", - zone: "example.com.", - nameservers: []string{":7053", ":8053", ":9053"}, - // use only the start of the message because the port changes with each call: 127.0.0.1:XXXXX->127.0.0.1:7053. - expectedError: "[fqdn=mail.example.com.] could not find the start of authority for 'mail.example.com.': DNS call error: read udp ", - }, - { - desc: "no nameservers", - fqdn: "test.example.com.", - zone: "example.com.", - nameservers: []string{}, - expectedError: "[fqdn=test.example.com.] could not find the start of authority for 'test.example.com.': empty list of nameservers", - }, - } +}{ + { + desc: "domain is a CNAME", + fqdn: "mail.google.com.", + zone: "google.com.", + primaryNs: "ns1.google.com.", + nameservers: recursiveNameservers, + }, + { + desc: "domain is a non-existent subdomain", + fqdn: "foo.google.com.", + zone: "google.com.", + primaryNs: "ns1.google.com.", + nameservers: recursiveNameservers, + }, + { + desc: "domain is a eTLD", + fqdn: "example.com.ac.", + zone: "ac.", + primaryNs: "a0.nic.ac.", + nameservers: recursiveNameservers, + }, + { + desc: "domain is a cross-zone CNAME", + fqdn: "cross-zone-example.assets.sh.", + zone: "assets.sh.", + primaryNs: "gina.ns.cloudflare.com.", + nameservers: recursiveNameservers, + }, + { + desc: "NXDOMAIN", + fqdn: "test.lego.zz.", + zone: "lego.zz.", + nameservers: []string{"8.8.8.8:53"}, + expectedError: "[fqdn=test.lego.zz.] could not find the start of authority for 'test.lego.zz.' [question='zz. IN SOA', code=NXDOMAIN]", + }, + { + desc: "several non existent nameservers", + fqdn: "mail.google.com.", + zone: "google.com.", + primaryNs: "ns1.google.com.", + nameservers: []string{":7053", ":8053", "8.8.8.8:53"}, + }, + { + desc: "only non-existent nameservers", + fqdn: "mail.google.com.", + zone: "google.com.", + nameservers: []string{":7053", ":8053", ":9053"}, + // use only the start of the message because the port changes with each call: 127.0.0.1:XXXXX->127.0.0.1:7053. + expectedError: "[fqdn=mail.google.com.] could not find the start of authority for 'mail.google.com.': DNS call error: read udp ", + }, + { + desc: "no nameservers", + fqdn: "test.ldez.com.", + zone: "ldez.com.", + nameservers: []string{}, + expectedError: "[fqdn=test.ldez.com.] could not find the start of authority for 'test.ldez.com.': empty list of nameservers", + }, } func TestFindZoneByFqdnCustom(t *testing.T) { - for _, test := range lookupSoaByFqdnTestCases(t) { + for _, test := range findXByFqdnTestCases { t.Run(test.desc, func(t *testing.T) { ClearFqdnCache() @@ -252,7 +153,7 @@ func TestFindZoneByFqdnCustom(t *testing.T) { } func TestFindPrimaryNsByFqdnCustom(t *testing.T) { - for _, test := range lookupSoaByFqdnTestCases(t) { + for _, test := range findXByFqdnTestCases { t.Run(test.desc, func(t *testing.T) { ClearFqdnCache() @@ -268,7 +169,7 @@ func TestFindPrimaryNsByFqdnCustom(t *testing.T) { } } -func Test_getNameservers_ResolveConfServers(t *testing.T) { +func TestResolveConfServers(t *testing.T) { testCases := []struct { fixture string expected []string diff --git a/challenge/dns01/precheck.go b/challenge/dns01/precheck.go index 45e17e3ac..f65dfb5af 100644 --- a/challenge/dns01/precheck.go +++ b/challenge/dns01/precheck.go @@ -4,15 +4,10 @@ import ( "fmt" "net" "strings" - "time" "github.com/miekg/dns" ) -// defaultNameserverPort used by authoritative NS. -// This is for tests only. -var defaultNameserverPort = "53" - // PreCheckFunc checks DNS propagation before notifying ACME that the DNS challenge is ready. type PreCheckFunc func(fqdn, value string) (bool, error) @@ -28,53 +23,23 @@ func WrapPreCheck(wrap WrapPreCheckFunc) ChallengeOption { } } -// DisableCompletePropagationRequirement obsolete. -// -// Deprecated: use DisableAuthoritativeNssPropagationRequirement instead. func DisableCompletePropagationRequirement() ChallengeOption { - return DisableAuthoritativeNssPropagationRequirement() -} - -func DisableAuthoritativeNssPropagationRequirement() ChallengeOption { return func(chlg *Challenge) error { - chlg.preCheck.requireAuthoritativeNssPropagation = false + chlg.preCheck.requireCompletePropagation = false return nil } } -func RecursiveNSsPropagationRequirement() ChallengeOption { - return func(chlg *Challenge) error { - chlg.preCheck.requireRecursiveNssPropagation = true - return nil - } -} - -func PropagationWait(wait time.Duration, skipCheck bool) ChallengeOption { - return WrapPreCheck(func(domain, fqdn, value string, check PreCheckFunc) (bool, error) { - time.Sleep(wait) - - if skipCheck { - return true, nil - } - - return check(fqdn, value) - }) -} - type preCheck struct { // checks DNS propagation before notifying ACME that the DNS challenge is ready. checkFunc WrapPreCheckFunc - // require the TXT record to be propagated to all authoritative name servers - requireAuthoritativeNssPropagation bool - - // require the TXT record to be propagated to all recursive name servers - requireRecursiveNssPropagation bool + requireCompletePropagation bool } func newPreCheck() preCheck { return preCheck{ - requireAuthoritativeNssPropagation: true, + requireCompletePropagation: true, } } @@ -88,48 +53,32 @@ func (p preCheck) call(domain, fqdn, value string) (bool, error) { // checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers. func (p preCheck) checkDNSPropagation(fqdn, value string) (bool, error) { - // Initial attempt to resolve at the recursive NS (require to get CNAME) + // Initial attempt to resolve at the recursive NS r, err := dnsQuery(fqdn, dns.TypeTXT, recursiveNameservers, true) if err != nil { - return false, fmt.Errorf("initial recursive nameserver: %w", err) + return false, err + } + + if !p.requireCompletePropagation { + return true, nil } if r.Rcode == dns.RcodeSuccess { fqdn = updateDomainWithCName(r, fqdn) } - if p.requireRecursiveNssPropagation { - _, err = checkNameserversPropagation(fqdn, value, recursiveNameservers, false) - if err != nil { - return false, fmt.Errorf("recursive nameservers: %w", err) - } - } - - if !p.requireAuthoritativeNssPropagation { - return true, nil - } - authoritativeNss, err := lookupNameservers(fqdn) if err != nil { return false, err } - found, err := checkNameserversPropagation(fqdn, value, authoritativeNss, true) - if err != nil { - return found, fmt.Errorf("authoritative nameservers: %w", err) - } - - return found, nil + return checkAuthoritativeNss(fqdn, value, authoritativeNss) } -// checkNameserversPropagation queries each of the given nameservers for the expected TXT record. -func checkNameserversPropagation(fqdn, value string, nameservers []string, addPort bool) (bool, error) { +// checkAuthoritativeNss queries each of the given nameservers for the expected TXT record. +func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, error) { for _, ns := range nameservers { - if addPort { - ns = net.JoinHostPort(ns, defaultNameserverPort) - } - - r, err := dnsQuery(fqdn, dns.TypeTXT, []string{ns}, false) + r, err := dnsQuery(fqdn, dns.TypeTXT, []string{net.JoinHostPort(ns, "53")}, false) if err != nil { return false, err } @@ -141,11 +90,9 @@ func checkNameserversPropagation(fqdn, value string, nameservers []string, addPo var records []string var found bool - for _, rr := range r.Answer { if txt, ok := rr.(*dns.TXT); ok { record := strings.Join(txt.Txt, "") - records = append(records, record) if record == value { found = true diff --git a/challenge/dns01/precheck_test.go b/challenge/dns01/precheck_test.go index bda8c781e..a2d9874b8 100644 --- a/challenge/dns01/precheck_test.go +++ b/challenge/dns01/precheck_test.go @@ -3,73 +3,40 @@ package dns01 import ( "testing" - "github.com/go-acme/lego/v4/platform/tester/dnsmock" - "github.com/miekg/dns" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func Test_preCheck_checkDNSPropagation(t *testing.T) { - mockResolver(t, - dnsmock.NewServer(). - Query("ns0.lego.localhost. A", - dnsmock.Answer(fakeA("ns0.lego.localhost.", "127.0.0.1"))). - Query("ns1.lego.localhost. A", - dnsmock.Answer(fakeA("ns1.lego.localhost.", "127.0.0.1"))). - Query("example.com. TXT", - dnsmock.Answer( - fakeTXT("example.com.", "one"), - fakeTXT("example.com.", "two"), - fakeTXT("example.com.", "three"), - fakeTXT("example.com.", "four"), - fakeTXT("example.com.", "five"), - ), - ). - Build(t), - ) - - useAsNameserver(t, - dnsmock.NewServer(). - Query("acme-staging.api.example.com. SOA", dnsmock.Error(dns.RcodeNameError)). - Query("api.example.com. SOA", dnsmock.Error(dns.RcodeNameError)). - Query("example.com. SOA", dnsmock.SOA("")). - Query("example.com. NS", - dnsmock.Answer( - fakeNS("example.com.", "ns0.lego.localhost."), - fakeNS("example.com.", "ns1.lego.localhost."), - ), - ). - Build(t), - ) - +func TestCheckDNSPropagation(t *testing.T) { testCases := []struct { - desc string - fqdn string - value string - expectedError string + desc string + fqdn string + value string + expectError bool }{ { desc: "success", - fqdn: "example.com.", - value: "four", + fqdn: "postman-echo.com.", + value: "postman-domain-verification=c85de626cb79d941310696e06558e2e790223802f3697dfbdcaf65510152d52c", }, { - desc: "no matching TXT record", - fqdn: "acme-staging.api.example.com.", - value: "fe01=", - expectedError: "did not return the expected TXT record [fqdn: acme-staging.api.example.com., value: fe01=]: one ,two ,three ,four ,five", + desc: "no TXT record", + fqdn: "acme-staging.api.letsencrypt.org.", + value: "fe01=", + expectError: true, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { + t.Parallel() ClearFqdnCache() check := newPreCheck() ok, err := check.checkDNSPropagation(test.fqdn, test.value) - if test.expectedError != "" { - assert.ErrorContainsf(t, err, test.expectedError, "PreCheckDNS must fail for %s", test.fqdn) + if test.expectError { + assert.Errorf(t, err, "PreCheckDNS must fail for %s", test.fqdn) assert.False(t, ok, "PreCheckDNS must fail for %s", test.fqdn) } else { assert.NoErrorf(t, err, "PreCheckDNS failed for %s", test.fqdn) @@ -79,67 +46,69 @@ func Test_preCheck_checkDNSPropagation(t *testing.T) { } } -func Test_checkNameserversPropagation_authoritativeNss(t *testing.T) { +func TestCheckAuthoritativeNss(t *testing.T) { testCases := []struct { - desc string - fqdn, value string - fakeDNSServer *dnsmock.Builder - expectedError string + desc string + fqdn, value string + ns []string + expected bool }{ { - desc: "TXT RR w/ expected value", - // NS: asnums.routeviews.org. - fqdn: "8.8.8.8.asn.routeviews.org.", - value: "151698.8.8.024", - fakeDNSServer: dnsmock.NewServer(). - Query("8.8.8.8.asn.routeviews.org. TXT", - dnsmock.Answer( - fakeTXT("8.8.8.8.asn.routeviews.org.", "151698.8.8.024"), - ), - ), - }, - { - desc: "TXT RR w/ unexpected value", - // NS: asnums.routeviews.org. - fqdn: "8.8.8.8.asn.routeviews.org.", - value: "fe01=", - fakeDNSServer: dnsmock.NewServer(). - Query("8.8.8.8.asn.routeviews.org. TXT", - dnsmock.Answer( - fakeTXT("8.8.8.8.asn.routeviews.org.", "15169"), - fakeTXT("8.8.8.8.asn.routeviews.org.", "8.8.8.0"), - fakeTXT("8.8.8.8.asn.routeviews.org.", "24"), - ), - ), - expectedError: "did not return the expected TXT record [fqdn: 8.8.8.8.asn.routeviews.org., value: fe01=]: 15169 ,8.8.8.0 ,24", + desc: "TXT RR w/ expected value", + fqdn: "8.8.8.8.asn.routeviews.org.", + value: "151698.8.8.024", + ns: []string{"asnums.routeviews.org."}, + expected: true, }, { desc: "No TXT RR", - // NS: ns2.google.com. - fqdn: "ns1.google.com.", - value: "fe01=", - fakeDNSServer: dnsmock.NewServer(). - Query("ns1.google.com.", dnsmock.Noop), - expectedError: "did not return the expected TXT record [fqdn: ns1.google.com., value: fe01=]: ", + fqdn: "ns1.google.com.", + ns: []string{"ns2.google.com."}, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { + t.Parallel() ClearFqdnCache() - addr := test.fakeDNSServer.Build(t) - - ok, err := checkNameserversPropagation(test.fqdn, test.value, []string{addr.String()}, false) - - if test.expectedError == "" { - require.NoError(t, err) - assert.True(t, ok) - } else { - require.Error(t, err) - require.ErrorContains(t, err, test.expectedError) - assert.False(t, ok) - } + ok, _ := checkAuthoritativeNss(test.fqdn, test.value, test.ns) + assert.Equal(t, test.expected, ok, test.fqdn) + }) + } +} + +func TestCheckAuthoritativeNssErr(t *testing.T) { + testCases := []struct { + desc string + fqdn, value string + ns []string + error string + }{ + { + desc: "TXT RR /w unexpected value", + fqdn: "8.8.8.8.asn.routeviews.org.", + value: "fe01=", + ns: []string{"asnums.routeviews.org."}, + error: "did not return the expected TXT record", + }, + { + desc: "No TXT RR", + fqdn: "ns1.google.com.", + value: "fe01=", + ns: []string{"ns2.google.com."}, + error: "did not return the expected TXT record", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + ClearFqdnCache() + + _, err := checkAuthoritativeNss(test.fqdn, test.value, test.ns) + require.Error(t, err) + assert.Contains(t, err.Error(), test.error) }) } } diff --git a/challenge/http01/domain_matcher.go b/challenge/http01/domain_matcher.go index 058d1a314..ae681c645 100644 --- a/challenge/http01/domain_matcher.go +++ b/challenge/http01/domain_matcher.go @@ -3,7 +3,6 @@ package http01 import ( "fmt" "net/http" - "net/netip" "strings" ) @@ -55,10 +54,10 @@ func (m *hostMatcher) name() string { } func (m *hostMatcher) matches(r *http.Request, domain string) bool { - return matchDomain(r.Host, domain) + return strings.HasPrefix(r.Host, domain) } -// arbitraryMatcher checks whether the specified (*net/http.Request).Header value starts with a domain name. +// hostMatcher checks whether the specified (*net/http.Request).Header value starts with a domain name. type arbitraryMatcher string func (m arbitraryMatcher) name() string { @@ -66,7 +65,7 @@ func (m arbitraryMatcher) name() string { } func (m arbitraryMatcher) matches(r *http.Request, domain string) bool { - return matchDomain(r.Header.Get(m.name()), domain) + return strings.HasPrefix(r.Header.Get(m.name()), domain) } // forwardedMatcher checks whether the Forwarded header contains a "host" element starting with a domain name. @@ -88,8 +87,7 @@ func (m *forwardedMatcher) matches(r *http.Request, domain string) bool { } host := fwds[0]["host"] - - return matchDomain(host, domain) + return strings.HasPrefix(host, domain) } // parsing requires some form of state machine. @@ -100,7 +98,6 @@ func parseForwardedHeader(s string) (elements []map[string]string, err error) { inquote := false pos := 0 - l := len(s) for i := 0; i < l; i++ { r := rune(s[i]) @@ -112,7 +109,6 @@ func parseForwardedHeader(s string) (elements []map[string]string, err error) { pos = i inquote = false } - continue } @@ -121,7 +117,6 @@ func parseForwardedHeader(s string) (elements []map[string]string, err error) { if key == "" { return nil, fmt.Errorf("unexpected quoted string as pos %d", i) } - inquote = true pos = i + 1 @@ -138,10 +133,11 @@ func parseForwardedHeader(s string) (elements []map[string]string, err error) { case r == ',': // end of forwarded-element if key != "" { - val = s[pos:i] + if val == "" { + val = s[pos:i] + } cur[key] = val } - elements = append(elements, cur) cur = make(map[string]string) key = "" @@ -164,14 +160,11 @@ func parseForwardedHeader(s string) (elements []map[string]string, err error) { if pos < len(s) { val = s[pos:] } - cur[key] = val } - if len(cur) > 0 { elements = append(elements, cur) } - return elements, nil } @@ -186,19 +179,9 @@ func skipWS(s string, i int) int { for isWS(rune(s[i+1])) { i++ } - return i } func isWS(r rune) bool { return strings.ContainsRune(" \t\v\r\n", r) } - -func matchDomain(src, domain string) bool { - addr, err := netip.ParseAddr(domain) - if err == nil && addr.Is6() { - domain = "[" + domain + "]" - } - - return strings.HasPrefix(src, domain) -} diff --git a/challenge/http01/domain_matcher_test.go b/challenge/http01/domain_matcher_test.go index 7bedf9f63..94add14bb 100644 --- a/challenge/http01/domain_matcher_test.go +++ b/challenge/http01/domain_matcher_test.go @@ -1,15 +1,13 @@ package http01 import ( - "net/http" - "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func Test_parseForwardedHeader(t *testing.T) { +func TestParseForwardedHeader(t *testing.T) { testCases := []struct { name string input string @@ -77,7 +75,7 @@ func Test_parseForwardedHeader(t *testing.T) { actual, err := parseForwardedHeader(test.input) if test.err == "" { require.NoError(t, err) - assert.Equal(t, test.want, actual) + assert.EqualValues(t, test.want, actual) } else { require.Error(t, err) assert.Contains(t, err.Error(), test.err) @@ -85,54 +83,3 @@ func Test_parseForwardedHeader(t *testing.T) { }) } } - -func Test_hostMatcher_matches(t *testing.T) { - hm := &hostMatcher{} - - testCases := []struct { - desc string - domain string - req *http.Request - expected assert.BoolAssertionFunc - }{ - { - desc: "exact domain", - domain: "example.com", - req: httptest.NewRequest(http.MethodGet, "http://example.com", nil), - expected: assert.True, - }, - { - desc: "request with path", - domain: "example.com", - req: httptest.NewRequest(http.MethodGet, "http://example.com/foo/bar", nil), - expected: assert.True, - }, - { - desc: "ipv4", - domain: "127.0.0.1", - req: httptest.NewRequest(http.MethodGet, "http://127.0.0.1", nil), - expected: assert.True, - }, - { - desc: "ipv6", - domain: "2001:db8::1", - req: httptest.NewRequest(http.MethodGet, "http://[2001:db8::1]", nil), - expected: assert.True, - }, - { - desc: "ipv6 with brackets", - domain: "[2001:db8::1]", - req: httptest.NewRequest(http.MethodGet, "http://[2001:db8::1]", nil), - expected: assert.True, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - hm.matches(test.req, test.domain) - - test.expected(t, hm.matches(test.req, test.domain)) - }) - } -} diff --git a/challenge/http01/http_challenge.go b/challenge/http01/http_challenge.go index a042979c2..f23e483cf 100644 --- a/challenge/http01/http_challenge.go +++ b/challenge/http01/http_challenge.go @@ -2,7 +2,6 @@ package http01 import ( "fmt" - "time" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api" @@ -12,16 +11,6 @@ import ( type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error -type ChallengeOption func(*Challenge) error - -// SetDelay sets a delay between the start of the HTTP server and the challenge validation. -func SetDelay(delay time.Duration) ChallengeOption { - return func(chlg *Challenge) error { - chlg.delay = delay - return nil - } -} - // ChallengePath returns the URL path for the `http-01` challenge. func ChallengePath(token string) string { return "/.well-known/acme-challenge/" + token @@ -31,24 +20,14 @@ type Challenge struct { core *api.Core validate ValidateFunc provider challenge.Provider - delay time.Duration } -func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider, opts ...ChallengeOption) *Challenge { - chlg := &Challenge{ +func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider) *Challenge { + return &Challenge{ core: core, validate: validate, provider: provider, } - - for _, opt := range opts { - err := opt(chlg) - if err != nil { - log.Infof("challenge option error: %v", err) - } - } - - return chlg } func (c *Challenge) SetProvider(provider challenge.Provider) { @@ -74,7 +53,6 @@ func (c *Challenge) Solve(authz acme.Authorization) error { if err != nil { return fmt.Errorf("[%s] acme: error presenting token: %w", domain, err) } - defer func() { err := c.provider.CleanUp(authz.Identifier.Value, chlng.Token, keyAuth) if err != nil { @@ -82,11 +60,6 @@ func (c *Challenge) Solve(authz acme.Authorization) error { } }() - if c.delay > 0 { - time.Sleep(c.delay) - } - chlng.KeyAuthorization = keyAuth - return c.validate(c.core, domain, chlng) } diff --git a/challenge/http01/http_challenge_server.go b/challenge/http01/http_challenge_server.go index ab962917e..f69f5ac1f 100644 --- a/challenge/http01/http_challenge_server.go +++ b/challenge/http01/http_challenge_server.go @@ -44,7 +44,6 @@ func NewUnixProviderServer(socketPath string, mode fs.FileMode) *ProviderServer // Present starts a web server and makes the token available at `ChallengePath(token)` for web requests. func (s *ProviderServer) Present(domain, token, keyAuth string) error { var err error - s.listener, err = net.Listen(s.network, s.GetAddress()) if err != nil { return fmt.Errorf("could not start HTTP server for challenge: %w", err) @@ -57,9 +56,7 @@ func (s *ProviderServer) Present(domain, token, keyAuth string) error { } s.done = make(chan bool) - go s.serve(domain, token, keyAuth) - return nil } @@ -72,11 +69,8 @@ func (s *ProviderServer) CleanUp(domain, token, keyAuth string) error { if s.listener == nil { return nil } - s.listener.Close() - <-s.done - return nil } @@ -113,24 +107,19 @@ func (s *ProviderServer) serve(domain, token, keyAuth string) { mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet && s.matcher.matches(r, domain) { w.Header().Set("Content-Type", "text/plain") - _, err := w.Write([]byte(keyAuth)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - log.Infof("[%s] Served key authentication", domain) - - return - } - - log.Warnf("Received request for domain %s with method %s but the domain did not match any challenge. Please ensure you are passing the %s header properly.", r.Host, r.Method, s.matcher.name()) - - _, err := w.Write([]byte("TEST")) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + } else { + log.Warnf("Received request for domain %s with method %s but the domain did not match any challenge. Please ensure you are passing the %s header properly.", r.Host, r.Method, s.matcher.name()) + _, err := w.Write([]byte("TEST")) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } } }) @@ -144,6 +133,5 @@ func (s *ProviderServer) serve(domain, token, keyAuth string) { if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { log.Println(err) } - s.done <- true } diff --git a/challenge/http01/http_challenge_test.go b/challenge/http01/http_challenge_test.go index 06c555e42..3a5aa6bbe 100644 --- a/challenge/http01/http_challenge_test.go +++ b/challenge/http01/http_challenge_test.go @@ -67,7 +67,7 @@ func TestProviderServer_GetAddress(t *testing.T) { } func TestChallenge(t *testing.T) { - server := tester.MockACMEServer().BuildHTTPS(t) + _, apiURL := tester.SetupFakeAPI(t) providerServer := NewProviderServer("", "23457") @@ -88,7 +88,6 @@ func TestChallenge(t *testing.T) { if err != nil { return err } - bodyStr := string(body) if bodyStr != chlng.KeyAuthorization { @@ -98,10 +97,10 @@ func TestChallenge(t *testing.T) { return nil } - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) + privateKey, err := rsa.GenerateKey(rand.Reader, 512) require.NoError(t, err, "Could not generate test key") - core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey) + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) solver := NewChallenge(core, validate, providerServer) @@ -124,7 +123,7 @@ func TestChallengeUnix(t *testing.T) { t.Skip("only for UNIX systems") } - server := tester.MockACMEServer().BuildHTTPS(t) + _, apiURL := tester.SetupFakeAPI(t) dir := t.TempDir() t.Cleanup(func() { _ = os.RemoveAll(dir) }) @@ -158,7 +157,6 @@ func TestChallengeUnix(t *testing.T) { if err != nil { return err } - bodyStr := string(body) if bodyStr != chlng.KeyAuthorization { @@ -168,10 +166,10 @@ func TestChallengeUnix(t *testing.T) { return nil } - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) + privateKey, err := rsa.GenerateKey(rand.Reader, 512) require.NoError(t, err, "Could not generate test key") - core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey) + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) solver := NewChallenge(core, validate, providerServer) @@ -190,12 +188,12 @@ func TestChallengeUnix(t *testing.T) { } func TestChallengeInvalidPort(t *testing.T) { - server := tester.MockACMEServer().BuildHTTPS(t) + _, apiURL := tester.SetupFakeAPI(t) - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) + privateKey, err := rsa.GenerateKey(rand.Reader, 128) require.NoError(t, err, "Could not generate test key") - core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey) + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) validate := func(_ *api.Core, _ string, _ acme.Challenge) error { return nil } @@ -226,7 +224,6 @@ func (h *testProxyHeader) update(r *http.Request) { if h == nil || len(h.values) == 0 { return } - if h.name == "Host" { r.Host = h.values[0] } else if h.name != "" { @@ -374,7 +371,7 @@ func TestChallengeWithProxy(t *testing.T) { func testServeWithProxy(t *testing.T, header, extra *testProxyHeader, expectError bool) { t.Helper() - server := tester.MockACMEServer().BuildHTTPS(t) + _, apiURL := tester.SetupFakeAPI(t) providerServer := NewProviderServer("localhost", "23457") if header != nil { @@ -388,7 +385,6 @@ func testServeWithProxy(t *testing.T, header, extra *testProxyHeader, expectErro if err != nil { return err } - header.update(req) extra.update(req) @@ -406,7 +402,6 @@ func testServeWithProxy(t *testing.T, header, extra *testProxyHeader, expectErro if err != nil { return err } - bodyStr := string(body) if bodyStr != chlng.KeyAuthorization { @@ -416,10 +411,10 @@ func testServeWithProxy(t *testing.T, header, extra *testProxyHeader, expectErro return nil } - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) + privateKey, err := rsa.GenerateKey(rand.Reader, 512) require.NoError(t, err, "Could not generate test key") - core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey) + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) solver := NewChallenge(core, validate, providerServer) diff --git a/challenge/resolver/errors.go b/challenge/resolver/errors.go index 65a6ccdb7..94ccbd76a 100644 --- a/challenge/resolver/errors.go +++ b/challenge/resolver/errors.go @@ -3,8 +3,6 @@ package resolver import ( "bytes" "fmt" - "maps" - "slices" "sort" ) @@ -18,16 +16,10 @@ func (e obtainError) Error() string { for domain := range e { domains = append(domains, domain) } - sort.Strings(domains) for _, domain := range domains { _, _ = fmt.Fprintf(buffer, "[%s] %s\n", domain, e[domain]) } - return buffer.String() } - -func (e obtainError) Unwrap() []error { - return slices.AppendSeq(make([]error, 0, len(e)), maps.Values(e)) -} diff --git a/challenge/resolver/errors_test.go b/challenge/resolver/errors_test.go deleted file mode 100644 index d4ab3c481..000000000 --- a/challenge/resolver/errors_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package resolver - -import ( - "errors" - "testing" - - "github.com/go-acme/lego/v4/acme" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func Test_obtainError_Error(t *testing.T) { - err := obtainError{ - "a": &acme.ProblemDetails{Type: "001"}, - "b": errors.New("oops"), - "c": errors.New("I did it again"), - } - - require.EqualError(t, err, `error: one or more domains had a problem: -[a] acme: error: 0 :: 001 :: -[b] oops -[c] I did it again -`) -} - -func Test_obtainError_Unwrap(t *testing.T) { - testCases := []struct { - desc string - err obtainError - assert assert.BoolAssertionFunc - }{ - { - desc: "one ok", - err: obtainError{ - "a": &acme.ProblemDetails{}, - "b": errors.New("oops"), - "c": errors.New("I did it again"), - }, - assert: assert.True, - }, - { - desc: "all ok", - err: obtainError{ - "a": &acme.ProblemDetails{Type: "001"}, - "b": &acme.ProblemDetails{Type: "002"}, - "c": &acme.ProblemDetails{Type: "002"}, - }, - assert: assert.True, - }, - { - desc: "nope", - err: obtainError{ - "a": errors.New("hello"), - "b": errors.New("oops"), - "c": errors.New("I did it again"), - }, - assert: assert.False, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - var pd *acme.ProblemDetails - - test.assert(t, errors.As(test.err, &pd)) - }) - } -} diff --git a/challenge/resolver/prober.go b/challenge/resolver/prober.go index 66b12c7a7..021facbb5 100644 --- a/challenge/resolver/prober.go +++ b/challenge/resolver/prober.go @@ -50,14 +50,11 @@ func NewProber(solverManager *SolverManager) *Prober { func (p *Prober) Solve(authorizations []acme.Authorization) error { failures := make(obtainError) - var ( - authSolvers []*selectedAuthSolver - authSolversSequential []*selectedAuthSolver - ) + var authSolvers []*selectedAuthSolver + var authSolversSequential []*selectedAuthSolver // Loop through the resources, basically through the domains. // First pass just selects a solver for each authz. - for _, authz := range authorizations { domain := challenge.GetTargetedDomain(authz) if authz.Status == acme.StatusValid { @@ -93,88 +90,47 @@ func (p *Prober) Solve(authorizations []acme.Authorization) error { if len(failures) > 0 { return failures } - return nil } func sequentialSolve(authSolvers []*selectedAuthSolver, failures obtainError) { - // Some CA are using the same token, - // this can be a problem with the DNS01 challenge when the DNS provider doesn't support duplicate TXT records. - // In the sequential mode, this is not a problem because we can solve the challenges in order. - // But it can reduce the number of call the DNS provider APIs. - uniq := make(map[string]struct{}) - for i, authSolver := range authSolvers { // Submit the challenge domain := challenge.GetTargetedDomain(authSolver.authz) - chlg, _ := challenge.FindChallenge(challenge.DNS01, authSolver.authz) - if solvr, ok := authSolver.solver.(preSolver); ok { - if _, ok := uniq[authSolver.authz.Identifier.Value+chlg.Token]; ok && chlg.Token != "" { - log.Infof("acme: duplicate token for %q (DNS-01); skipping pre-solve.", authSolver.authz.Identifier.Value) - continue - } - err := solvr.PreSolve(authSolver.authz) if err != nil { failures[domain] = err - cleanUp(authSolver.solver, authSolver.authz) - continue } - - uniq[authSolver.authz.Identifier.Value+chlg.Token] = struct{}{} } // Solve challenge err := authSolver.solver.Solve(authSolver.authz) if err != nil { failures[domain] = err - cleanUp(authSolver.solver, authSolver.authz) - continue } - if _, ok := uniq[authSolver.authz.Identifier.Value+chlg.Token]; ok || chlg.Token == "" { - // Clean challenge - cleanUp(authSolver.solver, authSolver.authz) + // Clean challenge + cleanUp(authSolver.solver, authSolver.authz) - if len(authSolvers)-1 > i { - solvr := authSolver.solver.(sequential) - _, interval := solvr.Sequential() - log.Infof("sequence: wait for %s", interval) - time.Sleep(interval) - } - - delete(uniq, authSolver.authz.Identifier.Value+chlg.Token) - } else { - log.Infof("acme: duplicate token for %q (DNS-01); skipping cleanup.", authSolver.authz.Identifier.Value) + if len(authSolvers)-1 > i { + solvr := authSolver.solver.(sequential) + _, interval := solvr.Sequential() + log.Infof("sequence: wait for %s", interval) + time.Sleep(interval) } } } func parallelSolve(authSolvers []*selectedAuthSolver, failures obtainError) { - // Some CA are using the same token, - // this can be a problem with the DNS01 challenge when the DNS provider doesn't support duplicate TXT records. - uniq := make(map[string]struct{}) - // For all valid preSolvers, first submit the challenges, so they have max time to propagate for _, authSolver := range authSolvers { authz := authSolver.authz - - chlg, err := challenge.FindChallenge(challenge.DNS01, authz) - if err == nil { - if _, ok := uniq[authz.Identifier.Value+chlg.Token]; ok { - log.Infof("acme: duplicate token for %q (DNS-01); skipping pre-solve.", authSolver.authz.Identifier.Value) - continue - } - - uniq[authz.Identifier.Value+chlg.Token] = struct{}{} - } - if solvr, ok := authSolver.solver.(preSolver); ok { err := solvr.PreSolve(authz) if err != nil { @@ -186,16 +142,6 @@ func parallelSolve(authSolvers []*selectedAuthSolver, failures obtainError) { defer func() { // Clean all created TXT records for _, authSolver := range authSolvers { - chlg, err := challenge.FindChallenge(challenge.DNS01, authSolver.authz) - if err == nil { - if _, ok := uniq[authSolver.authz.Identifier.Value+chlg.Token]; ok { - delete(uniq, authSolver.authz.Identifier.Value+chlg.Token) - } else { - log.Infof("acme: duplicate token for %q (DNS-01); skipping cleanup.", authSolver.authz.Identifier.Value) - continue - } - } - cleanUp(authSolver.solver, authSolver.authz) } }() @@ -203,7 +149,6 @@ func parallelSolve(authSolvers []*selectedAuthSolver, failures obtainError) { // Finally solve all challenges for real for _, authSolver := range authSolvers { authz := authSolver.authz - domain := challenge.GetTargetedDomain(authz) if failures[domain] != nil { // already failed in previous loop @@ -220,7 +165,6 @@ func parallelSolve(authSolvers []*selectedAuthSolver, failures obtainError) { func cleanUp(solvr solver, authz acme.Authorization) { if solvr, ok := solvr.(cleanup); ok { domain := challenge.GetTargetedDomain(authz) - err := solvr.CleanUp(authz) if err != nil { log.Warnf("[%s] acme: cleaning up failed: %v ", domain, err) diff --git a/challenge/resolver/prober_mock_test.go b/challenge/resolver/prober_mock_test.go index dc7ad8dec..5a91fe075 100644 --- a/challenge/resolver/prober_mock_test.go +++ b/challenge/resolver/prober_mock_test.go @@ -1,7 +1,6 @@ package resolver import ( - "fmt" "time" "github.com/go-acme/lego/v4/acme" @@ -12,68 +11,34 @@ type preSolverMock struct { preSolve map[string]error solve map[string]error cleanUp map[string]error - - preSolveCounter int - solveCounter int - cleanUpCounter int } func (s *preSolverMock) PreSolve(authorization acme.Authorization) error { - s.preSolveCounter++ - return s.preSolve[authorization.Identifier.Value] } func (s *preSolverMock) Solve(authorization acme.Authorization) error { - s.solveCounter++ - return s.solve[authorization.Identifier.Value] } func (s *preSolverMock) CleanUp(authorization acme.Authorization) error { - s.cleanUpCounter++ - return s.cleanUp[authorization.Identifier.Value] } -func (s *preSolverMock) String() string { - return fmt.Sprintf("PreSolve: %d, Solve: %d, CleanUp: %d", s.preSolveCounter, s.solveCounter, s.cleanUpCounter) -} - func createStubAuthorizationHTTP01(domain, status string) acme.Authorization { - return createStubAuthorization(domain, status, false, acme.Challenge{ - Type: challenge.HTTP01.String(), - Validated: time.Now(), - }) -} - -func createStubAuthorizationDNS01(domain string, wildcard bool) acme.Authorization { - var chlgs []acme.Challenge - - if wildcard { - chlgs = append(chlgs, acme.Challenge{ - Type: challenge.HTTP01.String(), - Validated: time.Now(), - }) - } - - chlgs = append(chlgs, acme.Challenge{ - Type: challenge.DNS01.String(), - Validated: time.Now(), - }) - - return createStubAuthorization(domain, acme.StatusProcessing, wildcard, chlgs...) -} - -func createStubAuthorization(domain, status string, wildcard bool, chlgs ...acme.Challenge) acme.Authorization { return acme.Authorization{ - Wildcard: wildcard, - Status: status, - Expires: time.Now(), + Status: status, + Expires: time.Now(), Identifier: acme.Identifier{ - Type: "dns", + Type: challenge.HTTP01.String(), Value: domain, }, - Challenges: chlgs, + Challenges: []acme.Challenge{ + { + Type: challenge.HTTP01.String(), + Validated: time.Now(), + Error: nil, + }, + }, } } diff --git a/challenge/resolver/prober_test.go b/challenge/resolver/prober_test.go index 829b16883..4ee9b1b46 100644 --- a/challenge/resolver/prober_test.go +++ b/challenge/resolver/prober_test.go @@ -2,22 +2,19 @@ package resolver import ( "errors" - "fmt" "testing" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/challenge" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestProber_Solve(t *testing.T) { testCases := []struct { - desc string - solvers map[challenge.Type]solver - authz []acme.Authorization - expectedError string - expectedCounters map[challenge.Type]string + desc string + solvers map[challenge.Type]solver + authz []acme.Authorization + expectedError string }{ { desc: "success", @@ -29,33 +26,9 @@ func TestProber_Solve(t *testing.T) { }, }, authz: []acme.Authorization{ - createStubAuthorizationHTTP01("example.com", acme.StatusProcessing), - createStubAuthorizationHTTP01("example.org", acme.StatusProcessing), - createStubAuthorizationHTTP01("example.net", acme.StatusProcessing), - }, - expectedCounters: map[challenge.Type]string{ - challenge.HTTP01: "PreSolve: 3, Solve: 3, CleanUp: 3", - }, - }, - { - desc: "DNS-01 deduplicate", - solvers: map[challenge.Type]solver{ - challenge.DNS01: &preSolverMock{ - preSolve: map[string]error{}, - solve: map[string]error{}, - cleanUp: map[string]error{}, - }, - }, - authz: []acme.Authorization{ - createStubAuthorizationDNS01("a.example", false), - createStubAuthorizationDNS01("a.example", true), - createStubAuthorizationDNS01("b.example", false), - createStubAuthorizationDNS01("b.example", true), - createStubAuthorizationDNS01("c.example", true), - createStubAuthorizationDNS01("d.example", false), - }, - expectedCounters: map[challenge.Type]string{ - challenge.DNS01: "PreSolve: 4, Solve: 6, CleanUp: 4", + createStubAuthorizationHTTP01("acme.wtf", acme.StatusProcessing), + createStubAuthorizationHTTP01("lego.wtf", acme.StatusProcessing), + createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusProcessing), }, }, { @@ -68,12 +41,9 @@ func TestProber_Solve(t *testing.T) { }, }, authz: []acme.Authorization{ - createStubAuthorizationHTTP01("example.com", acme.StatusValid), - createStubAuthorizationHTTP01("example.org", acme.StatusValid), - createStubAuthorizationHTTP01("example.net", acme.StatusValid), - }, - expectedCounters: map[challenge.Type]string{ - challenge.HTTP01: "PreSolve: 0, Solve: 0, CleanUp: 0", + createStubAuthorizationHTTP01("acme.wtf", acme.StatusValid), + createStubAuthorizationHTTP01("lego.wtf", acme.StatusValid), + createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusValid), }, }, { @@ -81,56 +51,50 @@ func TestProber_Solve(t *testing.T) { solvers: map[challenge.Type]solver{ challenge.HTTP01: &preSolverMock{ preSolve: map[string]error{ - "example.com": errors.New("preSolve error example.com"), + "acme.wtf": errors.New("preSolve error acme.wtf"), }, solve: map[string]error{ - "example.com": errors.New("solve error example.com"), + "acme.wtf": errors.New("solve error acme.wtf"), }, cleanUp: map[string]error{ - "example.com": errors.New("clean error example.com"), + "acme.wtf": errors.New("clean error acme.wtf"), }, }, }, authz: []acme.Authorization{ - createStubAuthorizationHTTP01("example.com", acme.StatusProcessing), - createStubAuthorizationHTTP01("example.org", acme.StatusProcessing), - createStubAuthorizationHTTP01("example.net", acme.StatusProcessing), + createStubAuthorizationHTTP01("acme.wtf", acme.StatusProcessing), + createStubAuthorizationHTTP01("lego.wtf", acme.StatusProcessing), + createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusProcessing), }, expectedError: `error: one or more domains had a problem: -[example.com] preSolve error example.com +[acme.wtf] preSolve error acme.wtf `, - expectedCounters: map[challenge.Type]string{ - challenge.HTTP01: "PreSolve: 3, Solve: 2, CleanUp: 3", - }, }, { desc: "errors at different stages", solvers: map[challenge.Type]solver{ challenge.HTTP01: &preSolverMock{ preSolve: map[string]error{ - "example.com": errors.New("preSolve error example.com"), + "acme.wtf": errors.New("preSolve error acme.wtf"), }, solve: map[string]error{ - "example.com": errors.New("solve error example.com"), - "example.org": errors.New("solve error example.org"), + "acme.wtf": errors.New("solve error acme.wtf"), + "lego.wtf": errors.New("solve error lego.wtf"), }, cleanUp: map[string]error{ - "example.net": errors.New("clean error example.net"), + "mydomain.wtf": errors.New("clean error mydomain.wtf"), }, }, }, authz: []acme.Authorization{ - createStubAuthorizationHTTP01("example.com", acme.StatusProcessing), - createStubAuthorizationHTTP01("example.org", acme.StatusProcessing), - createStubAuthorizationHTTP01("example.net", acme.StatusProcessing), + createStubAuthorizationHTTP01("acme.wtf", acme.StatusProcessing), + createStubAuthorizationHTTP01("lego.wtf", acme.StatusProcessing), + createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusProcessing), }, expectedError: `error: one or more domains had a problem: -[example.com] preSolve error example.com -[example.org] solve error example.org +[acme.wtf] preSolve error acme.wtf +[lego.wtf] solve error lego.wtf `, - expectedCounters: map[challenge.Type]string{ - challenge.HTTP01: "PreSolve: 3, Solve: 2, CleanUp: 3", - }, }, } @@ -148,10 +112,6 @@ func TestProber_Solve(t *testing.T) { } else { require.NoError(t, err) } - - for n, s := range test.solvers { - assert.Equal(t, test.expectedCounters[n], fmt.Sprintf("%s", s)) - } }) } } diff --git a/challenge/resolver/solver_manager.go b/challenge/resolver/solver_manager.go index 87cf6e2d8..138060bc7 100644 --- a/challenge/resolver/solver_manager.go +++ b/challenge/resolver/solver_manager.go @@ -1,13 +1,13 @@ package resolver import ( - "context" "errors" "fmt" "sort" + "strconv" "time" - "github.com/cenkalti/backoff/v5" + "github.com/cenkalti/backoff/v4" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/challenge" @@ -15,7 +15,6 @@ import ( "github.com/go-acme/lego/v4/challenge/http01" "github.com/go-acme/lego/v4/challenge/tlsalpn01" "github.com/go-acme/lego/v4/log" - "github.com/go-acme/lego/v4/platform/wait" ) type byType []acme.Challenge @@ -37,14 +36,14 @@ func NewSolversManager(core *api.Core) *SolverManager { } // SetHTTP01Provider specifies a custom provider p that can solve the given HTTP-01 challenge. -func (c *SolverManager) SetHTTP01Provider(p challenge.Provider, opts ...http01.ChallengeOption) error { - c.solvers[challenge.HTTP01] = http01.NewChallenge(c.core, validate, p, opts...) +func (c *SolverManager) SetHTTP01Provider(p challenge.Provider) error { + c.solvers[challenge.HTTP01] = http01.NewChallenge(c.core, validate, p) return nil } // SetTLSALPN01Provider specifies a custom provider p that can solve the given TLS-ALPN-01 challenge. -func (c *SolverManager) SetTLSALPN01Provider(p challenge.Provider, opts ...tlsalpn01.ChallengeOption) error { - c.solvers[challenge.TLSALPN01] = tlsalpn01.NewChallenge(c.core, validate, p, opts...) +func (c *SolverManager) SetTLSALPN01Provider(p challenge.Provider) error { + c.solvers[challenge.TLSALPN01] = tlsalpn01.NewChallenge(c.core, validate, p) return nil } @@ -70,7 +69,6 @@ func (c *SolverManager) chooseSolver(authz acme.Authorization) solver { log.Infof("[%s] acme: use %s solver", domain, chlg.Type) return solvr } - log.Infof("[%s] acme: Could not find solver for: %s", domain, chlg.Type) } @@ -93,20 +91,20 @@ func validate(core *api.Core, domain string, chlg acme.Challenge) error { return nil } - retryAfter, err := api.ParseRetryAfter(chlng.RetryAfter) - if err != nil || retryAfter == 0 { + ra, err := strconv.Atoi(chlng.RetryAfter) + if err != nil { // The ACME server MUST return a Retry-After. - // If it doesn't, or if it's invalid, we'll just poll hard. + // If it doesn't, we'll just poll hard. // Boulder does not implement the ability to retry challenges or the Retry-After header. // https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md#section-82 - retryAfter = 5 * time.Second + ra = 5 } - - ctx := context.Background() + initialInterval := time.Duration(ra) * time.Second bo := backoff.NewExponentialBackOff() - bo.InitialInterval = retryAfter - bo.MaxInterval = 10 * retryAfter + bo.InitialInterval = initialInterval + bo.MaxInterval = 10 * initialInterval + bo.MaxElapsedTime = 100 * initialInterval // After the path is sent, the ACME server will access our server. // Repeatedly check the server for an updated status on our request. @@ -126,12 +124,10 @@ func validate(core *api.Core, domain string, chlg acme.Challenge) error { return nil } - return fmt.Errorf("the server didn't respond to our request (status=%s)", authz.Status) + return errors.New("the server didn't respond to our request") } - return wait.Retry(ctx, operation, - backoff.WithBackOff(bo), - backoff.WithMaxElapsedTime(100*retryAfter)) + return backoff.Retry(operation, bo) } func checkChallengeStatus(chlng acme.ExtendedChallenge) (bool, error) { @@ -141,9 +137,9 @@ func checkChallengeStatus(chlng acme.ExtendedChallenge) (bool, error) { case acme.StatusPending, acme.StatusProcessing: return false, nil case acme.StatusInvalid: - return false, fmt.Errorf("invalid challenge: %w", chlng.Err()) + return false, chlng.Error default: - return false, fmt.Errorf("the server returned an unexpected challenge status: %s", chlng.Status) + return false, errors.New("the server returned an unexpected state") } } @@ -158,12 +154,11 @@ func checkAuthorizationStatus(authz acme.Authorization) (bool, error) { case acme.StatusInvalid: for _, chlg := range authz.Challenges { if chlg.Status == acme.StatusInvalid && chlg.Error != nil { - return false, fmt.Errorf("invalid authorization: %w", chlg.Err()) + return false, chlg.Error } } - - return false, errors.New("invalid authorization") + return false, fmt.Errorf("the authorization state %s", authz.Status) default: - return false, fmt.Errorf("the server returned an unexpected authorization status: %s", authz.Status) + return false, errors.New("the server returned an unexpected state") } } diff --git a/challenge/resolver/solver_manager_test.go b/challenge/resolver/solver_manager_test.go index 77149c73a..9249beeba 100644 --- a/challenge/resolver/solver_manager_test.go +++ b/challenge/resolver/solver_manager_test.go @@ -12,7 +12,6 @@ import ( "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/go-jose/go-jose/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -33,50 +32,70 @@ func TestByType(t *testing.T) { } func TestValidate(t *testing.T) { + mux, apiURL := tester.SetupFakeAPI(t) + var statuses []string - privateKey, _ := rsa.GenerateKey(rand.Reader, 1024) + privateKey, _ := rsa.GenerateKey(rand.Reader, 512) - server := tester.MockACMEServer(). - Route("POST /chlg", - http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - if err := validateNoBody(privateKey, req); err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) - return - } + mux.HandleFunc("/chlg", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } - rw.Header().Set("Link", - fmt.Sprintf(`; rel="up"`, req.Context().Value(http.LocalAddrContextKey))) + if err := validateNoBody(privateKey, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } - st := statuses[0] - statuses = statuses[1:] + w.Header().Set("Link", "<"+apiURL+`/my-authz>; rel="up"`) - chlg := &acme.Challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"} + st := statuses[0] + statuses = statuses[1:] - servermock.JSONEncode(chlg).ServeHTTP(rw, req) - })). - Route("POST /my-authz", - http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - st := statuses[0] - statuses = statuses[1:] + chlg := &acme.Challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"} + if st == acme.StatusInvalid { + chlg.Error = &acme.ProblemDetails{} + } - authorization := acme.Authorization{ - Status: st, - Challenges: []acme.Challenge{}, - } + err := tester.WriteJSONResponse(w, chlg) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) - if st == acme.StatusInvalid { - chlg := acme.Challenge{ - Status: acme.StatusInvalid, - } - authorization.Challenges = append(authorization.Challenges, chlg) - } + mux.HandleFunc("/my-authz", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } - servermock.JSONEncode(authorization).ServeHTTP(rw, req) - })). - BuildHTTPS(t) + st := statuses[0] + statuses = statuses[1:] - core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey) + authorization := acme.Authorization{ + Status: st, + Challenges: []acme.Challenge{}, + } + + if st == acme.StatusInvalid { + chlg := acme.Challenge{ + Status: acme.StatusInvalid, + Error: &acme.ProblemDetails{}, + } + authorization.Challenges = append(authorization.Challenges, chlg) + } + + err := tester.WriteJSONResponse(w, authorization) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) testCases := []struct { @@ -87,7 +106,7 @@ func TestValidate(t *testing.T) { { name: "POST-unexpected", statuses: []string{"weird"}, - want: "the server returned an unexpected challenge status: weird", + want: "unexpected", }, { name: "POST-valid", @@ -96,12 +115,12 @@ func TestValidate(t *testing.T) { { name: "POST-invalid", statuses: []string{acme.StatusInvalid}, - want: "invalid challenge:", + want: "error", }, { name: "POST-pending-unexpected", statuses: []string{acme.StatusPending, "weird"}, - want: "the server returned an unexpected authorization status: weird", + want: "unexpected", }, { name: "POST-pending-valid", @@ -110,7 +129,7 @@ func TestValidate(t *testing.T) { { name: "POST-pending-invalid", statuses: []string{acme.StatusPending, acme.StatusInvalid}, - want: "invalid authorization", + want: "error", }, } @@ -118,7 +137,7 @@ func TestValidate(t *testing.T) { t.Run(test.name, func(t *testing.T) { statuses = test.statuses - err := validate(core, "example.com", acme.Challenge{Type: "http-01", Token: "token", URL: server.URL + "/chlg"}) + err := validate(core, "example.com", acme.Challenge{Type: "http-01", Token: "token", URL: apiURL + "/chlg"}) if test.want == "" { require.NoError(t, err) } else { @@ -129,126 +148,6 @@ func TestValidate(t *testing.T) { } } -func Test_checkChallengeStatus(t *testing.T) { - testCases := []struct { - desc string - challenge acme.Challenge - requireErr require.ErrorAssertionFunc - expected bool - }{ - { - desc: "status valid", - challenge: acme.Challenge{Status: acme.StatusValid}, - requireErr: require.NoError, - expected: true, - }, - { - desc: "status invalid", - challenge: acme.Challenge{Status: acme.StatusInvalid}, - requireErr: require.Error, - expected: false, - }, - { - desc: "status invalid with error", - challenge: acme.Challenge{Status: acme.StatusInvalid, Error: &acme.ProblemDetails{}}, - requireErr: require.Error, - expected: false, - }, - { - desc: "status pending", - challenge: acme.Challenge{Status: acme.StatusPending}, - requireErr: require.NoError, - expected: false, - }, - { - desc: "status processing", - challenge: acme.Challenge{Status: acme.StatusProcessing}, - requireErr: require.NoError, - expected: false, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - status, err := checkChallengeStatus(acme.ExtendedChallenge{Challenge: test.challenge}) - test.requireErr(t, err) - - assert.Equal(t, test.expected, status) - }) - } -} - -func Test_checkAuthorizationStatus(t *testing.T) { - testCases := []struct { - desc string - authorization acme.Authorization - requireErr require.ErrorAssertionFunc - expected bool - }{ - { - desc: "status valid", - authorization: acme.Authorization{Status: acme.StatusValid}, - requireErr: require.NoError, - expected: true, - }, - { - desc: "status invalid", - authorization: acme.Authorization{Status: acme.StatusInvalid}, - requireErr: require.Error, - expected: false, - }, - { - desc: "status invalid with error", - authorization: acme.Authorization{Status: acme.StatusInvalid, Challenges: []acme.Challenge{{Error: &acme.ProblemDetails{}}}}, - requireErr: require.Error, - expected: false, - }, - { - desc: "status pending", - authorization: acme.Authorization{Status: acme.StatusPending}, - requireErr: require.NoError, - expected: false, - }, - { - desc: "status processing", - authorization: acme.Authorization{Status: acme.StatusProcessing}, - requireErr: require.NoError, - expected: false, - }, - { - desc: "status deactivated", - authorization: acme.Authorization{Status: acme.StatusDeactivated}, - requireErr: require.Error, - expected: false, - }, - { - desc: "status expired", - authorization: acme.Authorization{Status: acme.StatusExpired}, - requireErr: require.Error, - expected: false, - }, - { - desc: "status revoked", - authorization: acme.Authorization{Status: acme.StatusRevoked}, - requireErr: require.Error, - expected: false, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - status, err := checkAuthorizationStatus(test.authorization) - test.requireErr(t, err) - - assert.Equal(t, test.expected, status) - }) - } -} - // validateNoBody reads the http.Request POST body, parses the JWS and validates it to read the body. // If there is an error doing this, // or if the JWS body is not the empty JSON payload "{}" or a POST-as-GET payload "" an error is returned. @@ -260,7 +159,6 @@ func validateNoBody(privateKey *rsa.PrivateKey, r *http.Request) error { } sigAlgs := []jose.SignatureAlgorithm{jose.RS256} - jws, err := jose.ParseSigned(string(reqBody), sigAlgs) if err != nil { return err @@ -277,6 +175,5 @@ func validateNoBody(privateKey *rsa.PrivateKey, r *http.Request) error { if bodyStr := string(body); bodyStr != "{}" && bodyStr != "" { return fmt.Errorf(`expected JWS POST body "{}" or "", got %q`, bodyStr) } - return nil } diff --git a/challenge/tlsalpn01/tls_alpn_challenge.go b/challenge/tlsalpn01/tls_alpn_challenge.go index d8e939106..04ba71507 100644 --- a/challenge/tlsalpn01/tls_alpn_challenge.go +++ b/challenge/tlsalpn01/tls_alpn_challenge.go @@ -7,7 +7,6 @@ import ( "crypto/x509/pkix" "encoding/asn1" "fmt" - "time" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api" @@ -22,38 +21,18 @@ var idPeAcmeIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31} type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error -type ChallengeOption func(*Challenge) error - -// SetDelay sets a delay between the start of the TLS listener and the challenge validation. -func SetDelay(delay time.Duration) ChallengeOption { - return func(chlg *Challenge) error { - chlg.delay = delay - return nil - } -} - type Challenge struct { core *api.Core validate ValidateFunc provider challenge.Provider - delay time.Duration } -func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider, opts ...ChallengeOption) *Challenge { - chlg := &Challenge{ +func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider) *Challenge { + return &Challenge{ core: core, validate: validate, provider: provider, } - - for _, opt := range opts { - err := opt(chlg) - if err != nil { - log.Infof("challenge option error: %v", err) - } - } - - return chlg } func (c *Challenge) SetProvider(provider challenge.Provider) { @@ -80,7 +59,6 @@ func (c *Challenge) Solve(authz acme.Authorization) error { if err != nil { return fmt.Errorf("[%s] acme: error presenting token: %w", challenge.GetTargetedDomain(authz), err) } - defer func() { err := c.provider.CleanUp(domain, chlng.Token, keyAuth) if err != nil { @@ -88,12 +66,7 @@ func (c *Challenge) Solve(authz acme.Authorization) error { } }() - if c.delay > 0 { - time.Sleep(c.delay) - } - chlng.KeyAuthorization = keyAuth - return c.validate(c.core, domain, chlng) } diff --git a/challenge/tlsalpn01/tls_alpn_challenge_test.go b/challenge/tlsalpn01/tls_alpn_challenge_test.go index 59c2d61bc..8725a1360 100644 --- a/challenge/tlsalpn01/tls_alpn_challenge_test.go +++ b/challenge/tlsalpn01/tls_alpn_challenge_test.go @@ -8,6 +8,7 @@ import ( "crypto/tls" "encoding/asn1" "net" + "net/http" "testing" "github.com/go-acme/lego/v4/acme" @@ -20,7 +21,7 @@ import ( ) func TestChallenge(t *testing.T) { - server := tester.MockACMEServer().BuildHTTPS(t) + _, apiURL := tester.SetupFakeAPI(t) domain := "localhost" port := "24457" @@ -42,7 +43,6 @@ func TestChallenge(t *testing.T) { assert.NotEmpty(t, remoteCert.Extensions, "Expected the challenge certificate to contain extensions") idx := -1 - for i, ext := range remoteCert.Extensions { if idPeAcmeIdentifierV1.Equal(ext.Id) { idx = i @@ -66,10 +66,10 @@ func TestChallenge(t *testing.T) { return nil } - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) + privateKey, err := rsa.GenerateKey(rand.Reader, 512) require.NoError(t, err, "Could not generate test key") - core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey) + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) solver := NewChallenge( @@ -93,12 +93,12 @@ func TestChallenge(t *testing.T) { } func TestChallengeInvalidPort(t *testing.T) { - server := tester.MockACMEServer().BuildHTTPS(t) + _, apiURL := tester.SetupFakeAPI(t) - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) + privateKey, err := rsa.GenerateKey(rand.Reader, 128) require.NoError(t, err, "Could not generate test key") - core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey) + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) solver := NewChallenge( @@ -123,7 +123,7 @@ func TestChallengeInvalidPort(t *testing.T) { } func TestChallengeIPaddress(t *testing.T) { - server := tester.MockACMEServer().BuildHTTPS(t) + _, apiURL := tester.SetupFakeAPI(t) domain := "127.0.0.1" port := "24457" @@ -146,37 +146,31 @@ func TestChallengeIPaddress(t *testing.T) { assert.True(t, net.ParseIP("127.0.0.1").Equal(remoteCert.IPAddresses[0]), "challenge certificate IPAddress ") assert.NotEmpty(t, remoteCert.Extensions, "Expected the challenge certificate to contain extensions") - var ( - foundAcmeIdentifier bool - extValue []byte - ) - + var foundAcmeIdentifier bool + var extValue []byte for _, ext := range remoteCert.Extensions { if idPeAcmeIdentifierV1.Equal(ext.Id) { assert.True(t, ext.Critical, "Expected the challenge certificate id-pe-acmeIdentifier extension to be marked as critical") - foundAcmeIdentifier = true extValue = ext.Value - break } } require.True(t, foundAcmeIdentifier, "Expected the challenge certificate to contain an extension with the id-pe-acmeIdentifier id,") - zBytes := sha256.Sum256([]byte(chlng.KeyAuthorization)) value, err := asn1.Marshal(zBytes[:sha256.Size]) require.NoError(t, err, "Expected marshaling of the keyAuth to return no error") - require.Equal(t, value, extValue, "Expected the challenge certificate id-pe-acmeIdentifier extension to contain the SHA-256 digest of the keyAuth") + require.EqualValues(t, value, extValue, "Expected the challenge certificate id-pe-acmeIdentifier extension to contain the SHA-256 digest of the keyAuth") return nil } - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) + privateKey, err := rsa.GenerateKey(rand.Reader, 512) require.NoError(t, err, "Could not generate test key") - core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey) + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) solver := NewChallenge( diff --git a/cmd/accounts_storage.go b/cmd/accounts_storage.go index 01db2faf8..05cd23722 100644 --- a/cmd/accounts_storage.go +++ b/cmd/accounts_storage.go @@ -2,8 +2,10 @@ package cmd import ( "crypto" + "crypto/x509" "encoding/json" "encoding/pem" + "errors" "net/url" "os" "path/filepath" @@ -16,8 +18,6 @@ import ( "github.com/urfave/cli/v2" ) -const userIDPlaceholder = "noemail@example.com" - const ( baseAccountsRootFolderName = "accounts" baseKeysFolderName = "keys" @@ -34,7 +34,7 @@ const ( // // rootUserPath: // -// ./.lego/accounts/localhost_14000/foo@example.com/ +// ./.lego/accounts/localhost_14000/hubert@hubert.com/ // │ │ │ └── userID ("email" option) // │ │ └── CA server ("server" option) // │ └── root accounts directory @@ -42,7 +42,7 @@ const ( // // keysPath: // -// ./.lego/accounts/localhost_14000/foo@example.com/keys/ +// ./.lego/accounts/localhost_14000/hubert@hubert.com/keys/ // │ │ │ │ └── root keys directory // │ │ │ └── userID ("email" option) // │ │ └── CA server ("server" option) @@ -51,7 +51,7 @@ const ( // // accountFilePath: // -// ./.lego/accounts/localhost_14000/foo@example.com/account.json +// ./.lego/accounts/localhost_14000/hubert@hubert.com/account.json // │ │ │ │ └── account file // │ │ │ └── userID ("email" option) // │ │ └── CA server ("server" option) @@ -59,7 +59,6 @@ const ( // └── "path" option type AccountsStorage struct { userID string - email string rootPath string rootUserPath string keysPath string @@ -69,27 +68,21 @@ type AccountsStorage struct { // NewAccountsStorage Creates a new AccountsStorage. func NewAccountsStorage(ctx *cli.Context) *AccountsStorage { - // TODO: move to account struct? - email := ctx.String(flgEmail) + // TODO: move to account struct? Currently MUST pass email. + email := getEmail(ctx) - userID := email - if userID == "" { - userID = userIDPlaceholder - } - - serverURL, err := url.Parse(ctx.String(flgServer)) + serverURL, err := url.Parse(ctx.String("server")) if err != nil { log.Fatal(err) } - rootPath := filepath.Join(ctx.String(flgPath), baseAccountsRootFolderName) + rootPath := filepath.Join(ctx.String("path"), baseAccountsRootFolderName) serverPath := strings.NewReplacer(":", "_", "/", string(os.PathSeparator)).Replace(serverURL.Host) accountsPath := filepath.Join(rootPath, serverPath) - rootUserPath := filepath.Join(accountsPath, userID) + rootUserPath := filepath.Join(accountsPath, email) return &AccountsStorage{ - userID: userID, - email: email, + userID: email, rootPath: rootPath, rootUserPath: rootUserPath, keysPath: filepath.Join(rootUserPath, baseKeysFolderName), @@ -105,7 +98,6 @@ func (s *AccountsStorage) ExistsAccountFilePath() bool { } else if err != nil { log.Fatal(err) } - return true } @@ -121,10 +113,6 @@ func (s *AccountsStorage) GetUserID() string { return s.userID } -func (s *AccountsStorage) GetEmail() string { - return s.email -} - func (s *AccountsStorage) Save(account *Account) error { jsonBytes, err := json.MarshalIndent(account, "", "\t") if err != nil { @@ -137,14 +125,13 @@ func (s *AccountsStorage) Save(account *Account) error { func (s *AccountsStorage) LoadAccount(privateKey crypto.PrivateKey) *Account { fileBytes, err := os.ReadFile(s.accountFilePath) if err != nil { - log.Fatalf("Could not load file for account %s: %v", s.GetUserID(), err) + log.Fatalf("Could not load file for account %s: %v", s.userID, err) } var account Account - err = json.Unmarshal(fileBytes, &account) if err != nil { - log.Fatalf("Could not parse file for account %s: %v", s.GetUserID(), err) + log.Fatalf("Could not parse file for account %s: %v", s.userID, err) } account.key = privateKey @@ -152,14 +139,13 @@ func (s *AccountsStorage) LoadAccount(privateKey crypto.PrivateKey) *Account { if account.Registration == nil || account.Registration.Body.Status == "" { reg, err := tryRecoverRegistration(s.ctx, privateKey) if err != nil { - log.Fatalf("Could not load account for %s. Registration is nil: %#v", s.GetUserID(), err) + log.Fatalf("Could not load account for %s. Registration is nil: %#v", s.userID, err) } account.Registration = reg - err = s.Save(&account) if err != nil { - log.Fatalf("Could not save account for %s. Registration is nil: %#v", s.GetUserID(), err) + log.Fatalf("Could not save account for %s. Registration is nil: %#v", s.userID, err) } } @@ -167,19 +153,18 @@ func (s *AccountsStorage) LoadAccount(privateKey crypto.PrivateKey) *Account { } func (s *AccountsStorage) GetPrivateKey(keyType certcrypto.KeyType) crypto.PrivateKey { - accKeyPath := filepath.Join(s.keysPath, s.GetUserID()+".key") + accKeyPath := filepath.Join(s.keysPath, s.userID+".key") if _, err := os.Stat(accKeyPath); os.IsNotExist(err) { - log.Printf("No key found for account %s. Generating a %s key.", s.GetUserID(), keyType) + log.Printf("No key found for account %s. Generating a %s key.", s.userID, keyType) s.createKeysFolder() privateKey, err := generatePrivateKey(accKeyPath, keyType) if err != nil { - log.Fatalf("Could not generate RSA private account key for account %s: %v", s.GetUserID(), err) + log.Fatalf("Could not generate RSA private account key for account %s: %v", s.userID, err) } log.Printf("Saved key to %s", accKeyPath) - return privateKey } @@ -193,7 +178,7 @@ func (s *AccountsStorage) GetPrivateKey(keyType certcrypto.KeyType) crypto.Priva func (s *AccountsStorage) createKeysFolder() { if err := createNonExistingFolder(s.keysPath); err != nil { - log.Fatalf("Could not check/create directory for account %s: %v", s.GetUserID(), err) + log.Fatalf("Could not check/create directory for account %s: %v", s.userID, err) } } @@ -210,7 +195,6 @@ func generatePrivateKey(file string, keyType certcrypto.KeyType) (crypto.Private defer certOut.Close() pemKey := certcrypto.PEMBlock(privateKey) - err = pem.Encode(certOut, pemKey) if err != nil { return nil, err @@ -225,18 +209,22 @@ func loadPrivateKey(file string) (crypto.PrivateKey, error) { return nil, err } - privateKey, err := certcrypto.ParsePEMPrivateKey(keyBytes) - if err != nil { - return nil, err + keyBlock, _ := pem.Decode(keyBytes) + + switch keyBlock.Type { + case "RSA PRIVATE KEY": + return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) + case "EC PRIVATE KEY": + return x509.ParseECPrivateKey(keyBlock.Bytes) } - return privateKey, nil + return nil, errors.New("unknown private key type") } func tryRecoverRegistration(ctx *cli.Context, privateKey crypto.PrivateKey) (*registration.Resource, error) { // couldn't load account but got a key. Try to look the account up. config := lego.NewConfig(&Account{key: privateKey}) - config.CADirURL = ctx.String(flgServer) + config.CADirURL = ctx.String("server") config.UserAgent = getUserAgent(ctx) client, err := lego.NewClient(config) @@ -248,6 +236,5 @@ func tryRecoverRegistration(ctx *cli.Context, privateKey crypto.PrivateKey) (*re if err != nil { return nil, err } - return reg, nil } diff --git a/cmd/certs_storage.go b/cmd/certs_storage.go index 25ef58075..46f04bfe8 100644 --- a/cmd/certs_storage.go +++ b/cmd/certs_storage.go @@ -2,6 +2,7 @@ package cmd import ( "bytes" + "crypto" "crypto/x509" "encoding/json" "encoding/pem" @@ -60,7 +61,7 @@ type CertificatesStorage struct { // NewCertificatesStorage create a new certificates storage. func NewCertificatesStorage(ctx *cli.Context) *CertificatesStorage { - pfxFormat := ctx.String(flgPFXFormat) + pfxFormat := ctx.String("pfx.format") switch pfxFormat { case "DES", "RC2", "SHA256": @@ -69,13 +70,13 @@ func NewCertificatesStorage(ctx *cli.Context) *CertificatesStorage { } return &CertificatesStorage{ - rootPath: filepath.Join(ctx.String(flgPath), baseCertificatesFolderName), - archivePath: filepath.Join(ctx.String(flgPath), baseArchivesFolderName), - pem: ctx.Bool(flgPEM), - pfx: ctx.Bool(flgPFX), - pfxPassword: ctx.String(flgPFXPass), + rootPath: filepath.Join(ctx.String("path"), baseCertificatesFolderName), + archivePath: filepath.Join(ctx.String("path"), baseArchivesFolderName), + pem: ctx.Bool("pem"), + pfx: ctx.Bool("pfx"), + pfxPassword: ctx.String("pfx.pass"), pfxFormat: pfxFormat, - filename: ctx.String(flgFilename), + filename: ctx.String("filename"), } } @@ -158,7 +159,6 @@ func (s *CertificatesStorage) ExistsFile(domain, extension string) bool { } else if err != nil { log.Fatal(err) } - return true } @@ -233,9 +233,27 @@ func (s *CertificatesStorage) WritePFXFile(domain string, certRes *certificate.R return fmt.Errorf("unable to get certificate chain for domain %s: %w", domain, err) } - privateKey, err := certcrypto.ParsePEMPrivateKey(certRes.PrivateKey) - if err != nil { - return fmt.Errorf("unable to parse PrivateKey for domain %s: %w", domain, err) + keyPemBlock, _ := pem.Decode(certRes.PrivateKey) + if keyPemBlock == nil { + return fmt.Errorf("unable to parse PrivateKey for domain %s", domain) + } + + var privateKey crypto.Signer + var keyErr error + + switch keyPemBlock.Type { + case "RSA PRIVATE KEY": + privateKey, keyErr = x509.ParsePKCS1PrivateKey(keyPemBlock.Bytes) + if keyErr != nil { + return fmt.Errorf("unable to load RSA PrivateKey for domain %s: %w", domain, keyErr) + } + case "EC PRIVATE KEY": + privateKey, keyErr = x509.ParseECPrivateKey(keyPemBlock.Bytes) + if keyErr != nil { + return fmt.Errorf("unable to load EC PrivateKey for domain %s: %w", domain, keyErr) + } + default: + return fmt.Errorf("unsupported PrivateKey type '%s' for domain %s", keyPemBlock.Type, domain) } encoder, err := getPFXEncoder(s.pfxFormat) @@ -284,7 +302,6 @@ func getCertificateChain(certRes *certificate.Resource) ([]*x509.Certificate, er } var certChain []*x509.Certificate - for chainCertPemBlock != nil { chainCert, err := x509.ParseCertificate(chainCertPemBlock.Bytes) if err != nil { @@ -300,7 +317,6 @@ func getCertificateChain(certRes *certificate.Resource) ([]*x509.Certificate, er func getPFXEncoder(pfxFormat string) (*pkcs12.Encoder, error) { var encoder *pkcs12.Encoder - switch pfxFormat { case "SHA256": encoder = pkcs12.Modern2023 @@ -321,6 +337,5 @@ func sanitizedDomain(domain string) string { if err != nil { log.Fatal(err) } - return safe } diff --git a/cmd/cmd_before.go b/cmd/cmd_before.go index 04d8d3257..61ffcb97d 100644 --- a/cmd/cmd_before.go +++ b/cmd/cmd_before.go @@ -6,17 +6,17 @@ import ( ) func Before(ctx *cli.Context) error { - if ctx.String(flgPath) == "" { - log.Fatalf("Could not determine current working directory. Please pass --%s.", flgPath) + if ctx.String("path") == "" { + log.Fatal("Could not determine current working directory. Please pass --path.") } - err := createNonExistingFolder(ctx.String(flgPath)) + err := createNonExistingFolder(ctx.String("path")) if err != nil { log.Fatalf("Could not check/create path: %v", err) } - if ctx.String(flgServer) == "" { - log.Fatalf("Could not determine current working server. Please pass --%s.", flgServer) + if ctx.String("server") == "" { + log.Fatal("Could not determine current working server. Please pass --server.") } return nil diff --git a/cmd/cmd_dnshelp.go b/cmd/cmd_dnshelp.go index 41adf4c8d..e38e0a380 100644 --- a/cmd/cmd_dnshelp.go +++ b/cmd/cmd_dnshelp.go @@ -9,8 +9,6 @@ import ( "github.com/urfave/cli/v2" ) -const flgCode = "code" - func createDNSHelp() *cli.Command { return &cli.Command{ Name: "dnshelp", @@ -18,7 +16,7 @@ func createDNSHelp() *cli.Command { Action: dnsHelp, Flags: []cli.Flag{ &cli.StringFlag{ - Name: flgCode, + Name: "code", Aliases: []string{"c"}, Usage: fmt.Sprintf("DNS code: %s", allDNSCodes()), }, @@ -27,7 +25,7 @@ func createDNSHelp() *cli.Command { } func dnsHelp(ctx *cli.Context) error { - code := ctx.String(flgCode) + code := ctx.String("code") if code == "" { w := tabwriter.NewWriter(ctx.App.Writer, 0, 0, 2, ' ', 0) ew := &errWriter{w: w} @@ -58,7 +56,7 @@ type errWriter struct { err error } -func (ew *errWriter) writeln(a ...any) { +func (ew *errWriter) writeln(a ...interface{}) { if ew.err != nil { return } @@ -66,7 +64,7 @@ func (ew *errWriter) writeln(a ...any) { _, ew.err = fmt.Fprintln(ew.w, a...) } -func (ew *errWriter) writef(format string, a ...any) { +func (ew *errWriter) writef(format string, a ...interface{}) { if ew.err != nil { return } diff --git a/cmd/cmd_list.go b/cmd/cmd_list.go index 53cd12c3c..71daaf880 100644 --- a/cmd/cmd_list.go +++ b/cmd/cmd_list.go @@ -3,7 +3,6 @@ package cmd import ( "encoding/json" "fmt" - "net" "net/url" "os" "path/filepath" @@ -13,11 +12,6 @@ import ( "github.com/urfave/cli/v2" ) -const ( - flgAccounts = "accounts" - flgNames = "names" -) - func createList() *cli.Command { return &cli.Command{ Name: "list", @@ -25,19 +19,19 @@ func createList() *cli.Command { Action: list, Flags: []cli.Flag{ &cli.BoolFlag{ - Name: flgAccounts, + Name: "accounts", Aliases: []string{"a"}, Usage: "Display accounts.", }, &cli.BoolFlag{ - Name: flgNames, + Name: "names", Aliases: []string{"n"}, Usage: "Display certificate common names only.", }, // fake email, needed by NewAccountsStorage &cli.StringFlag{ - Name: flgEmail, - Value: "", + Name: "email", + Value: "unknown", Hidden: true, }, }, @@ -45,7 +39,7 @@ func createList() *cli.Command { } func list(ctx *cli.Context) error { - if ctx.Bool(flgAccounts) && !ctx.Bool(flgNames) { + if ctx.Bool("accounts") && !ctx.Bool("names") { if err := listAccount(ctx); err != nil { return err } @@ -62,13 +56,12 @@ func listCertificates(ctx *cli.Context) error { return err } - names := ctx.Bool(flgNames) + names := ctx.Bool("names") if len(matches) == 0 { if !names { fmt.Println("No certificates found.") } - return nil } @@ -77,7 +70,7 @@ func listCertificates(ctx *cli.Context) error { } for _, filename := range matches { - if strings.HasSuffix(filename, issuerExt) { + if strings.HasSuffix(filename, ".issuer.crt") { continue } @@ -101,11 +94,6 @@ func listCertificates(ctx *cli.Context) error { } else { fmt.Println(" Certificate Name:", name) fmt.Println(" Domains:", strings.Join(pCert.DNSNames, ", ")) - - if len(pCert.IPAddresses) > 0 { - fmt.Println(" IPs:", formatIPAddresses(pCert.IPAddresses)) - } - fmt.Println(" Expiry Date:", pCert.NotAfter) fmt.Println(" Certificate Path:", filename) fmt.Println() @@ -129,7 +117,6 @@ func listAccount(ctx *cli.Context) error { } fmt.Println("Found the following accounts:") - for _, filename := range matches { data, err := os.ReadFile(filename) if err != nil { @@ -137,7 +124,6 @@ func listAccount(ctx *cli.Context) error { } var account Account - err = json.Unmarshal(data, &account) if err != nil { return err @@ -156,12 +142,3 @@ func listAccount(ctx *cli.Context) error { return nil } - -func formatIPAddresses(ipAddresses []net.IP) string { - var ips []string - for _, ip := range ipAddresses { - ips = append(ips, ip.String()) - } - - return strings.Join(ips, ", ") -} diff --git a/cmd/cmd_renew.go b/cmd/cmd_renew.go index 4b41ebc78..ed6d728ea 100644 --- a/cmd/cmd_renew.go +++ b/cmd/cmd_renew.go @@ -6,7 +6,6 @@ import ( "errors" "math/rand" "os" - "slices" "time" "github.com/go-acme/lego/v4/acme/api" @@ -18,17 +17,14 @@ import ( "github.com/urfave/cli/v2" ) -// Flag names. const ( - flgRenewDays = "days" - flgRenewDynamic = "dynamic" - flgARIDisable = "ari-disable" - flgARIWaitToRenewDuration = "ari-wait-to-renew-duration" - flgReuseKey = "reuse-key" - flgRenewHook = "renew-hook" - flgRenewHookTimeout = "renew-hook-timeout" - flgNoRandomSleep = "no-random-sleep" - flgForceCertDomains = "force-cert-domains" + renewEnvAccountEmail = "LEGO_ACCOUNT_EMAIL" + renewEnvCertDomain = "LEGO_CERT_DOMAIN" + renewEnvCertPath = "LEGO_CERT_PATH" + renewEnvCertKeyPath = "LEGO_CERT_KEY_PATH" + renewEnvIssuerCertKeyPath = "LEGO_ISSUER_CERT_PATH" + renewEnvCertPEMPath = "LEGO_CERT_PEM_PATH" + renewEnvCertPFXPath = "LEGO_CERT_PFX_PATH" ) func createRenew() *cli.Command { @@ -38,103 +34,78 @@ func createRenew() *cli.Command { Action: renew, Before: func(ctx *cli.Context) error { // we require either domains or csr, but not both - hasDomains := len(ctx.StringSlice(flgDomains)) > 0 - - hasCsr := ctx.String(flgCSR) != "" + hasDomains := len(ctx.StringSlice("domains")) > 0 + hasCsr := ctx.String("csr") != "" if hasDomains && hasCsr { - log.Fatalf("Please specify either --%s/-d or --%s/-c, but not both", flgDomains, flgCSR) + log.Fatal("Please specify either --domains/-d or --csr/-c, but not both") } - if !hasDomains && !hasCsr { - log.Fatalf("Please specify --%s/-d (or --%s/-c if you already have a CSR)", flgDomains, flgCSR) + log.Fatal("Please specify --domains/-d (or --csr/-c if you already have a CSR)") } - - if ctx.Bool(flgForceCertDomains) && hasCsr { - log.Fatalf("--%s only works with --%s/-d, --%s/-c doesn't support this option.", flgForceCertDomains, flgDomains, flgCSR) - } - return nil }, Flags: []cli.Flag{ &cli.IntFlag{ - Name: flgRenewDays, + Name: "days", Value: 30, Usage: "The number of days left on a certificate to renew it.", }, - // TODO(ldez): in v5, remove this flag, use this behavior as default. &cli.BoolFlag{ - Name: flgRenewDynamic, - Value: false, - Usage: "Compute dynamically, based on the lifetime of the certificate(s), when to renew: use 1/3rd of the lifetime left, or 1/2 of the lifetime for short-lived certificates). This supersedes --days and will be the default behavior in Lego v5.", - }, - &cli.BoolFlag{ - Name: flgARIDisable, - Usage: "Do not use the renewalInfo endpoint (RFC9773) to check if a certificate should be renewed.", + Name: "ari-enable", + Usage: "Use the renewalInfo endpoint (draft-ietf-acme-ari) to check if a certificate should be renewed.", }, &cli.DurationFlag{ - Name: flgARIWaitToRenewDuration, + Name: "ari-wait-to-renew-duration", Usage: "The maximum duration you're willing to sleep for a renewal time returned by the renewalInfo endpoint.", }, &cli.BoolFlag{ - Name: flgReuseKey, + Name: "reuse-key", Usage: "Used to indicate you want to reuse your current private key for the new certificate.", }, &cli.BoolFlag{ - Name: flgNoBundle, + Name: "no-bundle", Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.", }, &cli.BoolFlag{ - Name: flgMustStaple, + Name: "must-staple", Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate." + " Only works if the CSR is generated by lego.", }, &cli.TimestampFlag{ - Name: flgNotBefore, + Name: "not-before", Usage: "Set the notBefore field in the certificate (RFC3339 format)", Layout: time.RFC3339, }, &cli.TimestampFlag{ - Name: flgNotAfter, + Name: "not-after", Usage: "Set the notAfter field in the certificate (RFC3339 format)", Layout: time.RFC3339, }, &cli.StringFlag{ - Name: flgPreferredChain, + Name: "preferred-chain", Usage: "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.", }, &cli.StringFlag{ - Name: flgProfile, - Usage: "If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.", - }, - &cli.StringFlag{ - Name: flgAlwaysDeactivateAuthorizations, + Name: "always-deactivate-authorizations", Usage: "Force the authorizations to be relinquished even if the certificate request was successful.", }, &cli.StringFlag{ - Name: flgRenewHook, + Name: "renew-hook", Usage: "Define a hook. The hook is executed only when the certificates are effectively renewed.", }, - &cli.DurationFlag{ - Name: flgRenewHookTimeout, - Usage: "Define the timeout for the hook execution.", - Value: 2 * time.Minute, - }, &cli.BoolFlag{ - Name: flgNoRandomSleep, + Name: "no-random-sleep", Usage: "Do not add a random sleep before the renewal." + " We do not recommend using this flag if you are doing your renewals in an automated way.", }, - &cli.BoolFlag{ - Name: flgForceCertDomains, - Usage: "Check and ensure that the cert's domain list matches those passed in the domains argument.", - }, }, } } func renew(ctx *cli.Context) error { - account, keyType := setupAccount(ctx, NewAccountsStorage(ctx)) + account, client := setup(ctx, NewAccountsStorage(ctx)) + setupChallenges(ctx, client) if account.Registration == nil { log.Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", account.Email) @@ -142,83 +113,59 @@ func renew(ctx *cli.Context) error { certsStorage := NewCertificatesStorage(ctx) - bundle := !ctx.Bool(flgNoBundle) + bundle := !ctx.Bool("no-bundle") - meta := map[string]string{ - hookEnvAccountEmail: account.Email, - } + meta := map[string]string{renewEnvAccountEmail: account.Email} // CSR - if ctx.IsSet(flgCSR) { - return renewForCSR(ctx, account, keyType, certsStorage, bundle, meta) + if ctx.IsSet("csr") { + return renewForCSR(ctx, client, certsStorage, bundle, meta) } // Domains - return renewForDomains(ctx, account, keyType, certsStorage, bundle, meta) + return renewForDomains(ctx, client, certsStorage, bundle, meta) } -func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error { - domains := ctx.StringSlice(flgDomains) +func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error { + domains := ctx.StringSlice("domains") domain := domains[0] // load the cert resource from files. // We store the certificate, private key and metadata in different files // as web servers would not be able to work with a combined file. - certificates, err := certsStorage.ReadCertificate(domain, certExt) + certificates, err := certsStorage.ReadCertificate(domain, ".crt") if err != nil { log.Fatalf("Error while loading the certificate for domain %s\n\t%v", domain, err) } cert := certificates[0] - var ( - ariRenewalTime *time.Time - replacesCertID string - ) - - var client *lego.Client - - if !ctx.Bool(flgARIDisable) { - client = setupClient(ctx, account, keyType) - + var ariRenewalTime *time.Time + if ctx.Bool("ari-enable") { ariRenewalTime = getARIRenewalTime(ctx, cert, domain, client) if ariRenewalTime != nil { now := time.Now().UTC() - // Figure out if we need to sleep before renewing. if ariRenewalTime.After(now) { log.Infof("[%s] Sleeping %s until renewal time %s", domain, ariRenewalTime.Sub(now), ariRenewalTime) time.Sleep(ariRenewalTime.Sub(now)) } } - - replacesCertID, err = certificate.MakeARICertID(cert) - if err != nil { - log.Fatalf("Error while construction the ARI CertID for domain %s\n\t%v", domain, err) - } } - forceDomains := ctx.Bool(flgForceCertDomains) - - certDomains := certcrypto.ExtractDomains(cert) - - if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgRenewDays), ctx.Bool(flgRenewDynamic)) && - (!forceDomains || slices.Equal(certDomains, domains)) { + if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int("days")) { return nil } - if client == nil { - client = setupClient(ctx, account, keyType) - } - // This is just meant to be informal for the user. timeLeft := cert.NotAfter.Sub(time.Now().UTC()) log.Infof("[%s] acme: Trying renewal with %d hours remaining", domain, int(timeLeft.Hours())) - var privateKey crypto.PrivateKey + certDomains := certcrypto.ExtractDomains(cert) - if ctx.Bool(flgReuseKey) { - keyBytes, errR := certsStorage.ReadFile(domain, keyExt) + var privateKey crypto.PrivateKey + if ctx.Bool("reuse-key") { + keyBytes, errR := certsStorage.ReadFile(domain, ".key") if errR != nil { log.Fatalf("Error while loading the private key for domain %s\n\t%v", domain, errR) } @@ -231,10 +178,9 @@ func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyT // https://github.com/go-acme/lego/issues/1656 // https://github.com/certbot/certbot/blob/284023a1b7672be2bd4018dd7623b3b92197d4b0/certbot/certbot/_internal/renewal.py#L435-L440 - if !isatty.IsTerminal(os.Stdout.Fd()) && !ctx.Bool(flgNoRandomSleep) { + if !isatty.IsTerminal(os.Stdout.Fd()) && !ctx.Bool("no-random-sleep") { // https://github.com/certbot/certbot/blob/284023a1b7672be2bd4018dd7623b3b92197d4b0/certbot/certbot/_internal/renewal.py#L472 const jitter = 8 * time.Minute - rnd := rand.New(rand.NewSource(time.Now().UnixNano())) sleepTime := time.Duration(rnd.Int63n(int64(jitter))) @@ -242,25 +188,22 @@ func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyT time.Sleep(sleepTime) } - renewalDomains := slices.Clone(domains) - if !forceDomains { - renewalDomains = merge(certDomains, domains) - } - request := certificate.ObtainRequest{ - Domains: renewalDomains, + Domains: merge(certDomains, domains), PrivateKey: privateKey, - MustStaple: ctx.Bool(flgMustStaple), - NotBefore: getTime(ctx, flgNotBefore), - NotAfter: getTime(ctx, flgNotAfter), + MustStaple: ctx.Bool("must-staple"), + NotBefore: getTime(ctx, "not-before"), + NotAfter: getTime(ctx, "not-after"), Bundle: bundle, - PreferredChain: ctx.String(flgPreferredChain), - Profile: ctx.String(flgProfile), - AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations), + PreferredChain: ctx.String("preferred-chain"), + AlwaysDeactivateAuthorizations: ctx.Bool("always-deactivate-authorizations"), } - if replacesCertID != "" { - request.ReplacesCertID = replacesCertID + if ctx.Bool("ari-enable") { + request.ReplacesCertID, err = certificate.MakeARICertID(cert) + if err != nil { + log.Fatalf("Error while construction the ARI CertID for domain %s\n\t%v", domain, err) + } } certRes, err := client.Certificate.Obtain(request) @@ -268,17 +211,15 @@ func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyT log.Fatal(err) } - certRes.Domain = domain - certsStorage.SaveResource(certRes) addPathToMetadata(meta, domain, certRes, certsStorage) - return launchHook(ctx.String(flgRenewHook), ctx.Duration(flgRenewHookTimeout), meta) + return launchHook(ctx.String("renew-hook"), meta) } -func renewForCSR(ctx *cli.Context, account *Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error { - csr, err := readCSRFile(ctx.String(flgCSR)) +func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error { + csr, err := readCSRFile(ctx.String("csr")) if err != nil { log.Fatal(err) } @@ -291,64 +232,48 @@ func renewForCSR(ctx *cli.Context, account *Account, keyType certcrypto.KeyType, // load the cert resource from files. // We store the certificate, private key and metadata in different files // as web servers would not be able to work with a combined file. - certificates, err := certsStorage.ReadCertificate(domain, certExt) + certificates, err := certsStorage.ReadCertificate(domain, ".crt") if err != nil { log.Fatalf("Error while loading the certificate for domain %s\n\t%v", domain, err) } cert := certificates[0] - var ( - ariRenewalTime *time.Time - replacesCertID string - ) - - var client *lego.Client - - if !ctx.Bool(flgARIDisable) { - client = setupClient(ctx, account, keyType) - + var ariRenewalTime *time.Time + if ctx.Bool("ari-enable") { ariRenewalTime = getARIRenewalTime(ctx, cert, domain, client) if ariRenewalTime != nil { now := time.Now().UTC() - // Figure out if we need to sleep before renewing. if ariRenewalTime.After(now) { log.Infof("[%s] Sleeping %s until renewal time %s", domain, ariRenewalTime.Sub(now), ariRenewalTime) time.Sleep(ariRenewalTime.Sub(now)) } } - - replacesCertID, err = certificate.MakeARICertID(cert) - if err != nil { - log.Fatalf("Error while construction the ARI CertID for domain %s\n\t%v", domain, err) - } } - if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgRenewDays), ctx.Bool(flgRenewDynamic)) { + if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int("days")) { return nil } - if client == nil { - client = setupClient(ctx, account, keyType) - } - // This is just meant to be informal for the user. timeLeft := cert.NotAfter.Sub(time.Now().UTC()) log.Infof("[%s] acme: Trying renewal with %d hours remaining", domain, int(timeLeft.Hours())) request := certificate.ObtainForCSRRequest{ CSR: csr, - NotBefore: getTime(ctx, flgNotBefore), - NotAfter: getTime(ctx, flgNotAfter), + NotBefore: getTime(ctx, "not-before"), + NotAfter: getTime(ctx, "not-after"), Bundle: bundle, - PreferredChain: ctx.String(flgPreferredChain), - Profile: ctx.String(flgProfile), - AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations), + PreferredChain: ctx.String("preferred-chain"), + AlwaysDeactivateAuthorizations: ctx.Bool("always-deactivate-authorizations"), } - if replacesCertID != "" { - request.ReplacesCertID = replacesCertID + if ctx.Bool("ari-enable") { + request.ReplacesCertID, err = certificate.MakeARICertID(cert) + if err != nil { + log.Fatalf("Error while construction the ARI CertID for domain %s\n\t%v", domain, err) + } } certRes, err := client.Certificate.ObtainForCSR(request) @@ -360,51 +285,24 @@ func renewForCSR(ctx *cli.Context, account *Account, keyType certcrypto.KeyType, addPathToMetadata(meta, domain, certRes, certsStorage) - return launchHook(ctx.String(flgRenewHook), ctx.Duration(flgRenewHookTimeout), meta) + return launchHook(ctx.String("renew-hook"), meta) } -func needRenewal(x509Cert *x509.Certificate, domain string, days int, dynamic bool) bool { +func needRenewal(x509Cert *x509.Certificate, domain string, days int) bool { if x509Cert.IsCA { log.Fatalf("[%s] Certificate bundle starts with a CA certificate", domain) } - if dynamic { - return needRenewalDynamic(x509Cert, domain, time.Now()) + if days >= 0 { + notAfter := int(time.Until(x509Cert.NotAfter).Hours() / 24.0) + if notAfter > days { + log.Printf("[%s] The certificate expires in %d days, the number of days defined to perform the renewal is %d: no renewal.", + domain, notAfter, days) + return false + } } - if days < 0 { - return true - } - - notAfter := int(time.Until(x509Cert.NotAfter).Hours() / 24.0) - if notAfter <= days { - return true - } - - log.Printf("[%s] The certificate expires in %d days, the number of days defined to perform the renewal is %d: no renewal.", - domain, notAfter, days) - - return false -} - -func needRenewalDynamic(x509Cert *x509.Certificate, domain string, now time.Time) bool { - lifetime := x509Cert.NotAfter.Sub(x509Cert.NotBefore) - - var divisor int64 = 3 - if lifetime.Round(24*time.Hour).Hours()/24.0 <= 10 { - divisor = 2 - } - - dueDate := x509Cert.NotAfter.Add(-1 * time.Duration(lifetime.Nanoseconds()/divisor)) - - if dueDate.Before(now) { - return true - } - - log.Infof("[%s] The certificate expires at %s, the renewal can be performed in %s: no renewal.", - domain, x509Cert.NotAfter.Format(time.RFC3339), dueDate.Sub(now)) - - return false + return true } // getARIRenewalTime checks if the certificate needs to be renewed using the renewalInfo endpoint. @@ -420,20 +318,16 @@ func getARIRenewalTime(ctx *cli.Context, cert *x509.Certificate, domain string, log.Warnf("[%s] acme: %v", domain, err) return nil } - log.Warnf("[%s] acme: calling renewal info endpoint: %v", domain, err) - return nil } now := time.Now().UTC() - - renewalTime := renewalInfo.ShouldRenewAt(now, ctx.Duration(flgARIWaitToRenewDuration)) + renewalTime := renewalInfo.ShouldRenewAt(now, ctx.Duration("ari-wait-to-renew-duration")) if renewalTime == nil { log.Infof("[%s] acme: renewalInfo endpoint indicates that renewal is not needed", domain) return nil } - log.Infof("[%s] acme: renewalInfo endpoint indicates that renewal is needed", domain) if renewalInfo.ExplanationURL != "" { @@ -443,14 +337,36 @@ func getARIRenewalTime(ctx *cli.Context, cert *x509.Certificate, domain string, return renewalTime } -func merge(prevDomains, nextDomains []string) []string { - for _, next := range nextDomains { - if slices.Contains(prevDomains, next) { - continue - } +func addPathToMetadata(meta map[string]string, domain string, certRes *certificate.Resource, certsStorage *CertificatesStorage) { + meta[renewEnvCertDomain] = domain + meta[renewEnvCertPath] = certsStorage.GetFileName(domain, certExt) + meta[renewEnvCertKeyPath] = certsStorage.GetFileName(domain, keyExt) - prevDomains = append(prevDomains, next) + if certRes.IssuerCertificate != nil { + meta[renewEnvIssuerCertKeyPath] = certsStorage.GetFileName(domain, issuerExt) } + if certsStorage.pem { + meta[renewEnvCertPEMPath] = certsStorage.GetFileName(domain, pemExt) + } + + if certsStorage.pfx { + meta[renewEnvCertPFXPath] = certsStorage.GetFileName(domain, pfxExt) + } +} + +func merge(prevDomains, nextDomains []string) []string { + for _, next := range nextDomains { + var found bool + for _, prev := range prevDomains { + if prev == next { + found = true + break + } + } + if !found { + prevDomains = append(prevDomains, next) + } + } return prevDomains } diff --git a/cmd/cmd_renew_test.go b/cmd/cmd_renew_test.go index 2485c5240..f88ad74c5 100644 --- a/cmd/cmd_renew_test.go +++ b/cmd/cmd_renew_test.go @@ -108,62 +108,9 @@ func Test_needRenewal(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - actual := needRenewal(test.x509Cert, "foo.com", test.days, false) + actual := needRenewal(test.x509Cert, "foo.com", test.days) assert.Equal(t, test.expected, actual) }) } } - -func Test_needRenewalDynamic(t *testing.T) { - testCases := []struct { - desc string - now time.Time - notBefore, notAfter time.Time - expected assert.BoolAssertionFunc - }{ - { - desc: "higher than 1/3 of the certificate lifetime left (lifetime > 10 days)", - now: time.Date(2025, 1, 19, 1, 1, 1, 1, time.UTC), - notBefore: time.Date(2025, 1, 1, 1, 1, 1, 1, time.UTC), - notAfter: time.Date(2025, 1, 30, 1, 1, 1, 1, time.UTC), - expected: assert.False, - }, - { - desc: "lower than 1/3 of the certificate lifetime left(lifetime > 10 days)", - now: time.Date(2025, 1, 21, 1, 1, 1, 1, time.UTC), - notBefore: time.Date(2025, 1, 1, 1, 1, 1, 1, time.UTC), - notAfter: time.Date(2025, 1, 30, 1, 1, 1, 1, time.UTC), - expected: assert.True, - }, - { - desc: "higher than 1/2 of the certificate lifetime left (lifetime < 10 days)", - now: time.Date(2025, 1, 4, 1, 1, 1, 1, time.UTC), - notBefore: time.Date(2025, 1, 1, 1, 1, 1, 1, time.UTC), - notAfter: time.Date(2025, 1, 10, 1, 1, 1, 1, time.UTC), - expected: assert.False, - }, - { - desc: "lower than 1/2 of the certificate lifetime left (lifetime < 10 days)", - now: time.Date(2025, 1, 6, 1, 1, 1, 1, time.UTC), - notBefore: time.Date(2025, 1, 1, 1, 1, 1, 1, time.UTC), - notAfter: time.Date(2025, 1, 10, 1, 1, 1, 1, time.UTC), - expected: assert.True, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - x509Cert := &x509.Certificate{ - NotBefore: test.notBefore, - NotAfter: test.notAfter, - } - - ok := needRenewalDynamic(x509Cert, "example.com", test.now) - - test.expected(t, ok) - }) - } -} diff --git a/cmd/cmd_revoke.go b/cmd/cmd_revoke.go index 667bebe12..cd95978d2 100644 --- a/cmd/cmd_revoke.go +++ b/cmd/cmd_revoke.go @@ -6,12 +6,6 @@ import ( "github.com/urfave/cli/v2" ) -// Flag names. -const ( - flgKeep = "keep" - flgReason = "reason" -) - func createRevoke() *cli.Command { return &cli.Command{ Name: "revoke", @@ -19,12 +13,12 @@ func createRevoke() *cli.Command { Action: revoke, Flags: []cli.Flag{ &cli.BoolFlag{ - Name: flgKeep, + Name: "keep", Aliases: []string{"k"}, Usage: "Keep the certificates after the revocation instead of archiving them.", }, &cli.UintFlag{ - Name: flgReason, + Name: "reason", Usage: "Identifies the reason for the certificate revocation." + " See https://www.rfc-editor.org/rfc/rfc5280.html#section-5.3.1." + " Valid values are:" + @@ -38,26 +32,24 @@ func createRevoke() *cli.Command { } func revoke(ctx *cli.Context) error { - account, keyType := setupAccount(ctx, NewAccountsStorage(ctx)) + acc, client := setup(ctx, NewAccountsStorage(ctx)) - if account.Registration == nil { - log.Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", account.Email) + if acc.Registration == nil { + log.Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", acc.Email) } - client := newClient(ctx, account, keyType) - certsStorage := NewCertificatesStorage(ctx) certsStorage.CreateRootFolder() - for _, domain := range ctx.StringSlice(flgDomains) { + for _, domain := range ctx.StringSlice("domains") { log.Printf("Trying to revoke certificate for domain %s", domain) - certBytes, err := certsStorage.ReadFile(domain, certExt) + certBytes, err := certsStorage.ReadFile(domain, ".crt") if err != nil { log.Fatalf("Error while revoking the certificate for domain %s\n\t%v", domain, err) } - reason := ctx.Uint(flgReason) + reason := ctx.Uint("reason") err = client.Certificate.RevokeWithReason(certBytes, &reason) if err != nil { @@ -66,7 +58,7 @@ func revoke(ctx *cli.Context) error { log.Println("Certificate was revoked.") - if ctx.Bool(flgKeep) { + if ctx.Bool("keep") { return nil } diff --git a/cmd/cmd_run.go b/cmd/cmd_run.go index 5924c4b66..10bd0cbc3 100644 --- a/cmd/cmd_run.go +++ b/cmd/cmd_run.go @@ -14,107 +14,76 @@ import ( "github.com/urfave/cli/v2" ) -// Flag names. -const ( - flgNoBundle = "no-bundle" - flgMustStaple = "must-staple" - flgNotBefore = "not-before" - flgNotAfter = "not-after" - flgPrivateKey = "private-key" - flgPreferredChain = "preferred-chain" - flgProfile = "profile" - flgAlwaysDeactivateAuthorizations = "always-deactivate-authorizations" - flgRunHook = "run-hook" - flgRunHookTimeout = "run-hook-timeout" -) - func createRun() *cli.Command { return &cli.Command{ Name: "run", Usage: "Register an account, then create and install a certificate", Before: func(ctx *cli.Context) error { // we require either domains or csr, but not both - hasDomains := len(ctx.StringSlice(flgDomains)) > 0 - - hasCsr := ctx.String(flgCSR) != "" + hasDomains := len(ctx.StringSlice("domains")) > 0 + hasCsr := ctx.String("csr") != "" if hasDomains && hasCsr { log.Fatal("Please specify either --domains/-d or --csr/-c, but not both") } - if !hasDomains && !hasCsr { log.Fatal("Please specify --domains/-d (or --csr/-c if you already have a CSR)") } - return nil }, Action: run, Flags: []cli.Flag{ &cli.BoolFlag{ - Name: flgNoBundle, + Name: "no-bundle", Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.", }, &cli.BoolFlag{ - Name: flgMustStaple, + Name: "must-staple", Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate." + " Only works if the CSR is generated by lego.", }, &cli.TimestampFlag{ - Name: flgNotBefore, + Name: "not-before", Usage: "Set the notBefore field in the certificate (RFC3339 format)", Layout: time.RFC3339, }, &cli.TimestampFlag{ - Name: flgNotAfter, + Name: "not-after", Usage: "Set the notAfter field in the certificate (RFC3339 format)", Layout: time.RFC3339, }, &cli.StringFlag{ - Name: flgPrivateKey, - Usage: "Path to private key (in PEM encoding) for the certificate. By default, the private key is generated.", - }, - &cli.StringFlag{ - Name: flgPreferredChain, + Name: "preferred-chain", Usage: "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.", }, &cli.StringFlag{ - Name: flgProfile, - Usage: "If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.", - }, - &cli.StringFlag{ - Name: flgAlwaysDeactivateAuthorizations, + Name: "always-deactivate-authorizations", Usage: "Force the authorizations to be relinquished even if the certificate request was successful.", }, &cli.StringFlag{ - Name: flgRunHook, + Name: "run-hook", Usage: "Define a hook. The hook is executed when the certificates are effectively created.", }, - &cli.DurationFlag{ - Name: flgRunHookTimeout, - Usage: "Define the timeout for the hook execution.", - Value: 2 * time.Minute, - }, }, } } const rootPathWarningMessage = `!!!! HEADS UP !!!! -Your account credentials have been saved in your +Your account credentials have been saved in your Let's Encrypt configuration directory at "%s". You should make a secure backup of this folder now. This -configuration directory will also contain private keys -generated by lego and certificates obtained from the ACME -server. Making regular backups of this folder is ideal. +configuration directory will also contain certificates and +private keys obtained from Let's Encrypt so making regular +backups of this folder is ideal. ` func run(ctx *cli.Context) error { accountsStorage := NewAccountsStorage(ctx) - account, keyType := setupAccount(ctx, accountsStorage) - - client := setupClient(ctx, account, keyType) + account, client := setup(ctx, accountsStorage) + setupChallenges(ctx, client) if account.Registration == nil { reg, err := register(ctx, client) @@ -143,27 +112,28 @@ func run(ctx *cli.Context) error { certsStorage.SaveResource(cert) meta := map[string]string{ - hookEnvAccountEmail: account.Email, + renewEnvAccountEmail: account.Email, + renewEnvCertDomain: cert.Domain, + renewEnvCertPath: certsStorage.GetFileName(cert.Domain, ".crt"), + renewEnvCertKeyPath: certsStorage.GetFileName(cert.Domain, ".key"), + renewEnvCertPEMPath: certsStorage.GetFileName(cert.Domain, ".pem"), + renewEnvCertPFXPath: certsStorage.GetFileName(cert.Domain, ".pfx"), } - addPathToMetadata(meta, cert.Domain, cert, certsStorage) - - return launchHook(ctx.String(flgRunHook), ctx.Duration(flgRunHookTimeout), meta) + return launchHook(ctx.String("run-hook"), meta) } func handleTOS(ctx *cli.Context, client *lego.Client) bool { // Check for a global accept override - if ctx.Bool(flgAcceptTOS) { + if ctx.Bool("accept-tos") { return true } reader := bufio.NewReader(os.Stdin) - log.Printf("Please review the TOS at %s", client.GetToSURL()) for { fmt.Println("Do you accept the TOS? Y/n") - text, err := reader.ReadString('\n') if err != nil { log.Fatalf("Could not read from console: %v", err) @@ -187,12 +157,12 @@ func register(ctx *cli.Context, client *lego.Client) (*registration.Resource, er log.Fatal("You did not accept the TOS. Unable to proceed.") } - if ctx.Bool(flgEAB) { - kid := ctx.String(flgKID) - hmacEncoded := ctx.String(flgHMAC) + if ctx.Bool("eab") { + kid := ctx.String("kid") + hmacEncoded := ctx.String("hmac") if kid == "" || hmacEncoded == "" { - log.Fatalf("Requires arguments --%s and --%s.", flgKID, flgHMAC) + log.Fatalf("Requires arguments --kid and --hmac.") } return client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ @@ -206,36 +176,34 @@ func register(ctx *cli.Context, client *lego.Client) (*registration.Resource, er } func obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Resource, error) { - bundle := !ctx.Bool(flgNoBundle) + bundle := !ctx.Bool("no-bundle") - domains := ctx.StringSlice(flgDomains) + domains := ctx.StringSlice("domains") if len(domains) > 0 { // obtain a certificate, generating a new private key request := certificate.ObtainRequest{ Domains: domains, - MustStaple: ctx.Bool(flgMustStaple), - NotBefore: getTime(ctx, flgNotBefore), - NotAfter: getTime(ctx, flgNotAfter), Bundle: bundle, - PreferredChain: ctx.String(flgPreferredChain), - Profile: ctx.String(flgProfile), - AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations), + MustStaple: ctx.Bool("must-staple"), + PreferredChain: ctx.String("preferred-chain"), + AlwaysDeactivateAuthorizations: ctx.Bool("always-deactivate-authorizations"), } - if ctx.IsSet(flgPrivateKey) { - var err error + notBefore := ctx.Timestamp("not-before") + if notBefore != nil { + request.NotBefore = *notBefore + } - request.PrivateKey, err = loadPrivateKey(ctx.String(flgPrivateKey)) - if err != nil { - return nil, fmt.Errorf("load private key: %w", err) - } + notAfter := ctx.Timestamp("not-after") + if notAfter != nil { + request.NotAfter = *notAfter } return client.Certificate.Obtain(request) } // read the CSR - csr, err := readCSRFile(ctx.String(flgCSR)) + csr, err := readCSRFile(ctx.String("csr")) if err != nil { return nil, err } @@ -243,21 +211,11 @@ func obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Reso // obtain a certificate for this CSR request := certificate.ObtainForCSRRequest{ CSR: csr, - NotBefore: getTime(ctx, flgNotBefore), - NotAfter: getTime(ctx, flgNotAfter), + NotBefore: getTime(ctx, "not-before"), + NotAfter: getTime(ctx, "not-after"), Bundle: bundle, - PreferredChain: ctx.String(flgPreferredChain), - Profile: ctx.String(flgProfile), - AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations), - } - - if ctx.IsSet(flgPrivateKey) { - var err error - - request.PrivateKey, err = loadPrivateKey(ctx.String(flgPrivateKey)) - if err != nil { - return nil, fmt.Errorf("load private key: %w", err) - } + PreferredChain: ctx.String("preferred-chain"), + AlwaysDeactivateAuthorizations: ctx.Bool("always-deactivate-authorizations"), } return client.Certificate.ObtainForCSR(request) diff --git a/cmd/flags.go b/cmd/flags.go index c7e8371b6..1d8ca58e0 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -1,253 +1,161 @@ package cmd import ( - "fmt" "time" - "github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/lego" "github.com/urfave/cli/v2" "software.sslmate.com/src/go-pkcs12" ) -// Flag names. -const ( - flgDomains = "domains" - flgServer = "server" - flgAcceptTOS = "accept-tos" - flgEmail = "email" - flgDisableCommonName = "disable-cn" - flgCSR = "csr" - flgEAB = "eab" - flgKID = "kid" - flgHMAC = "hmac" - flgKeyType = "key-type" - flgFilename = "filename" - flgPath = "path" - flgHTTP = "http" - flgHTTPPort = "http.port" - flgHTTPDelay = "http.delay" - flgHTTPProxyHeader = "http.proxy-header" - flgHTTPWebroot = "http.webroot" - flgHTTPMemcachedHost = "http.memcached-host" - flgHTTPS3Bucket = "http.s3-bucket" - flgTLS = "tls" - flgTLSPort = "tls.port" - flgTLSDelay = "tls.delay" - flgDNS = "dns" - flgDNSDisableCP = "dns.disable-cp" - flgDNSPropagationWait = "dns.propagation-wait" - flgDNSPropagationDisableANS = "dns.propagation-disable-ans" - flgDNSPropagationRNS = "dns.propagation-rns" - flgDNSResolvers = "dns.resolvers" - flgHTTPTimeout = "http-timeout" - flgTLSSkipVerify = "tls-skip-verify" - flgDNSTimeout = "dns-timeout" - flgPEM = "pem" - flgPFX = "pfx" - flgPFXPass = "pfx.pass" - flgPFXFormat = "pfx.format" - flgCertTimeout = "cert.timeout" - flgOverallRequestLimit = "overall-request-limit" - flgUserAgent = "user-agent" -) - -const ( - envEAB = "LEGO_EAB" - envEABHMAC = "LEGO_EAB_HMAC" - envEABKID = "LEGO_EAB_KID" - envEmail = "LEGO_EMAIL" - envPath = "LEGO_PATH" - envPFX = "LEGO_PFX" - envPFXFormat = "LEGO_PFX_FORMAT" - envPFXPassword = "LEGO_PFX_PASSWORD" - envServer = "LEGO_SERVER" -) - func CreateFlags(defaultPath string) []cli.Flag { return []cli.Flag{ &cli.StringSliceFlag{ - Name: flgDomains, + Name: "domains", Aliases: []string{"d"}, Usage: "Add a domain to the process. Can be specified multiple times.", }, &cli.StringFlag{ - Name: flgServer, + Name: "server", Aliases: []string{"s"}, - EnvVars: []string{envServer}, + EnvVars: []string{"LEGO_SERVER"}, Usage: "CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client.", Value: lego.LEDirectoryProduction, }, &cli.BoolFlag{ - Name: flgAcceptTOS, + Name: "accept-tos", Aliases: []string{"a"}, Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.", }, &cli.StringFlag{ - Name: flgEmail, + Name: "email", Aliases: []string{"m"}, - EnvVars: []string{envEmail}, Usage: "Email used for registration and recovery contact.", }, - &cli.BoolFlag{ - Name: flgDisableCommonName, - Usage: "Disable the use of the common name in the CSR.", - }, &cli.StringFlag{ - Name: flgCSR, + Name: "csr", Aliases: []string{"c"}, Usage: "Certificate signing request filename, if an external CSR is to be used.", }, &cli.BoolFlag{ - Name: flgEAB, - EnvVars: []string{envEAB}, + Name: "eab", + EnvVars: []string{"LEGO_EAB"}, Usage: "Use External Account Binding for account registration. Requires --kid and --hmac.", }, &cli.StringFlag{ - Name: flgKID, - EnvVars: []string{envEABKID}, + Name: "kid", + EnvVars: []string{"LEGO_EAB_KID"}, Usage: "Key identifier from External CA. Used for External Account Binding.", }, &cli.StringFlag{ - Name: flgHMAC, - EnvVars: []string{envEABHMAC}, + Name: "hmac", + EnvVars: []string{"LEGO_EAB_HMAC"}, Usage: "MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding.", }, &cli.StringFlag{ - Name: flgKeyType, + Name: "key-type", Aliases: []string{"k"}, Value: "ec256", Usage: "Key type to use for private keys. Supported: rsa2048, rsa3072, rsa4096, rsa8192, ec256, ec384.", }, &cli.StringFlag{ - Name: flgFilename, + Name: "filename", Usage: "(deprecated) Filename of the generated certificate.", }, &cli.StringFlag{ - Name: flgPath, - EnvVars: []string{envPath}, + Name: "path", + EnvVars: []string{"LEGO_PATH"}, Usage: "Directory to use for storing the data.", Value: defaultPath, }, &cli.BoolFlag{ - Name: flgHTTP, + Name: "http", Usage: "Use the HTTP-01 challenge to solve challenges. Can be mixed with other types of challenges.", }, &cli.StringFlag{ - Name: flgHTTPPort, + Name: "http.port", Usage: "Set the port and interface to use for HTTP-01 based challenges to listen on. Supported: interface:port or :port.", Value: ":80", }, - &cli.DurationFlag{ - Name: flgHTTPDelay, - Usage: "Delay between the starts of the HTTP server (use for HTTP-01 based challenges) and the validation of the challenge.", - Value: 0, - }, &cli.StringFlag{ - Name: flgHTTPProxyHeader, + Name: "http.proxy-header", Usage: "Validate against this HTTP header when solving HTTP-01 based challenges behind a reverse proxy.", Value: "Host", }, &cli.StringFlag{ - Name: flgHTTPWebroot, + Name: "http.webroot", Usage: "Set the webroot folder to use for HTTP-01 based challenges to write directly to the .well-known/acme-challenge file." + " This disables the built-in server and expects the given directory to be publicly served with access to .well-known/acme-challenge", }, &cli.StringSliceFlag{ - Name: flgHTTPMemcachedHost, + Name: "http.memcached-host", Usage: "Set the memcached host(s) to use for HTTP-01 based challenges. Challenges will be written to all specified hosts.", }, &cli.StringFlag{ - Name: flgHTTPS3Bucket, + Name: "http.s3-bucket", Usage: "Set the S3 bucket name to use for HTTP-01 based challenges. Challenges will be written to the S3 bucket.", }, &cli.BoolFlag{ - Name: flgTLS, + Name: "tls", Usage: "Use the TLS-ALPN-01 challenge to solve challenges. Can be mixed with other types of challenges.", }, &cli.StringFlag{ - Name: flgTLSPort, + Name: "tls.port", Usage: "Set the port and interface to use for TLS-ALPN-01 based challenges to listen on. Supported: interface:port or :port.", Value: ":443", }, - &cli.DurationFlag{ - Name: flgTLSDelay, - Usage: "Delay between the start of the TLS listener (use for TLSALPN-01 based challenges) and the validation of the challenge.", - Value: 0, - }, &cli.StringFlag{ - Name: flgDNS, + Name: "dns", Usage: "Solve a DNS-01 challenge using the specified provider. Can be mixed with other types of challenges. Run 'lego dnshelp' for help on usage.", }, &cli.BoolFlag{ - Name: flgDNSDisableCP, - Usage: fmt.Sprintf("(deprecated) use %s instead.", flgDNSPropagationDisableANS), - }, - &cli.BoolFlag{ - Name: flgDNSPropagationDisableANS, + Name: "dns.disable-cp", Usage: "By setting this flag to true, disables the need to await propagation of the TXT record to all authoritative name servers.", }, - &cli.BoolFlag{ - Name: flgDNSPropagationRNS, - Usage: "By setting this flag to true, use all the recursive nameservers to check the propagation of the TXT record.", - }, - &cli.DurationFlag{ - Name: flgDNSPropagationWait, - Usage: "By setting this flag, disables all the propagation checks of the TXT record and uses a wait duration instead.", - }, &cli.StringSliceFlag{ - Name: flgDNSResolvers, + Name: "dns.resolvers", Usage: "Set the resolvers to use for performing (recursive) CNAME resolving and apex domain determination." + " For DNS-01 challenge verification, the authoritative DNS server is queried directly." + " Supported: host:port." + " The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined.", }, &cli.IntFlag{ - Name: flgHTTPTimeout, + Name: "http-timeout", Usage: "Set the HTTP timeout value to a specific value in seconds.", }, - &cli.BoolFlag{ - Name: flgTLSSkipVerify, - Usage: "Skip the TLS verification of the ACME server.", - }, &cli.IntFlag{ - Name: flgDNSTimeout, + Name: "dns-timeout", Usage: "Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name server queries.", Value: 10, }, &cli.BoolFlag{ - Name: flgPEM, + Name: "pem", Usage: "Generate an additional .pem (base64) file by concatenating the .key and .crt files together.", }, &cli.BoolFlag{ - Name: flgPFX, + Name: "pfx", Usage: "Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together.", - EnvVars: []string{envPFX}, + EnvVars: []string{"LEGO_PFX"}, }, &cli.StringFlag{ - Name: flgPFXPass, + Name: "pfx.pass", Usage: "The password used to encrypt the .pfx (PCKS#12) file.", Value: pkcs12.DefaultPassword, - EnvVars: []string{envPFXPassword}, + EnvVars: []string{"LEGO_PFX_PASSWORD"}, }, &cli.StringFlag{ - Name: flgPFXFormat, + Name: "pfx.format", Usage: "The encoding format to use when encrypting the .pfx (PCKS#12) file. Supported: RC2, DES, SHA256.", Value: "RC2", - EnvVars: []string{envPFXFormat}, + EnvVars: []string{"LEGO_PFX_FORMAT"}, }, &cli.IntFlag{ - Name: flgCertTimeout, + Name: "cert.timeout", Usage: "Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates.", Value: 30, }, - &cli.IntFlag{ - Name: flgOverallRequestLimit, - Usage: "ACME overall requests limit.", - Value: certificate.DefaultOverallRequestLimit, - }, &cli.StringFlag{ - Name: flgUserAgent, + Name: "user-agent", Usage: "Add to the user-agent sent to the CA to identify an application embedding lego-cli", }, } @@ -258,6 +166,5 @@ func getTime(ctx *cli.Context, name string) time.Time { if value == nil { return time.Time{} } - return *value } diff --git a/cmd/hook.go b/cmd/hook.go index 7883108b6..0b0ca4038 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -1,7 +1,6 @@ package cmd import ( - "bufio" "context" "errors" "fmt" @@ -9,70 +8,32 @@ import ( "os/exec" "strings" "time" - - "github.com/go-acme/lego/v4/certificate" ) -const ( - hookEnvAccountEmail = "LEGO_ACCOUNT_EMAIL" - hookEnvCertDomain = "LEGO_CERT_DOMAIN" - hookEnvCertPath = "LEGO_CERT_PATH" - hookEnvCertKeyPath = "LEGO_CERT_KEY_PATH" - hookEnvIssuerCertKeyPath = "LEGO_ISSUER_CERT_PATH" - hookEnvCertPEMPath = "LEGO_CERT_PEM_PATH" - hookEnvCertPFXPath = "LEGO_CERT_PFX_PATH" -) - -func launchHook(hook string, timeout time.Duration, meta map[string]string) error { +func launchHook(hook string, meta map[string]string) error { if hook == "" { return nil } - ctxCmd, cancel := context.WithTimeout(context.Background(), timeout) + ctxCmd, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() parts := strings.Fields(hook) - cmd := exec.CommandContext(ctxCmd, parts[0], parts[1:]...) + cmdCtx := exec.CommandContext(ctxCmd, parts[0], parts[1:]...) + cmdCtx.Env = append(os.Environ(), metaToEnv(meta)...) - cmd.Env = append(os.Environ(), metaToEnv(meta)...) + output, err := cmdCtx.CombinedOutput() - stdout, err := cmd.StdoutPipe() - if err != nil { - return fmt.Errorf("create pipe: %w", err) + if len(output) > 0 { + fmt.Println(string(output)) } - cmd.Stderr = cmd.Stdout - - err = cmd.Start() - if err != nil { - return fmt.Errorf("start command: %w", err) + if errors.Is(ctxCmd.Err(), context.DeadlineExceeded) { + return errors.New("hook timed out") } - go func() { - <-ctxCmd.Done() - - if ctxCmd.Err() != nil { - _ = cmd.Process.Kill() - _ = stdout.Close() - } - }() - - scanner := bufio.NewScanner(stdout) - for scanner.Scan() { - fmt.Println(scanner.Text()) - } - - err = cmd.Wait() - if err != nil { - if errors.Is(ctxCmd.Err(), context.DeadlineExceeded) { - return errors.New("hook timed out") - } - - return fmt.Errorf("wait command: %w", err) - } - - return nil + return err } func metaToEnv(meta map[string]string) []string { @@ -84,21 +45,3 @@ func metaToEnv(meta map[string]string) []string { return envs } - -func addPathToMetadata(meta map[string]string, domain string, certRes *certificate.Resource, certsStorage *CertificatesStorage) { - meta[hookEnvCertDomain] = domain - meta[hookEnvCertPath] = certsStorage.GetFileName(domain, certExt) - meta[hookEnvCertKeyPath] = certsStorage.GetFileName(domain, keyExt) - - if certRes.IssuerCertificate != nil { - meta[hookEnvIssuerCertKeyPath] = certsStorage.GetFileName(domain, issuerExt) - } - - if certsStorage.pem { - meta[hookEnvCertPEMPath] = certsStorage.GetFileName(domain, pemExt) - } - - if certsStorage.pfx { - meta[hookEnvCertPFXPath] = certsStorage.GetFileName(domain, pfxExt) - } -} diff --git a/cmd/hook_test.go b/cmd/hook_test.go deleted file mode 100644 index d643bba30..000000000 --- a/cmd/hook_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package cmd - -import ( - "runtime" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func Test_launchHook(t *testing.T) { - err := launchHook("echo foo", 1*time.Second, map[string]string{}) - require.NoError(t, err) -} - -func Test_launchHook_errors(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("skipping test on Windows") - } - - testCases := []struct { - desc string - hook string - timeout time.Duration - expected string - }{ - { - desc: "kill the hook", - hook: "sleep 5", - timeout: 1 * time.Second, - expected: "hook timed out", - }, - { - desc: "context timeout on Start", - hook: "echo foo", - timeout: 1 * time.Nanosecond, - expected: "start command: context deadline exceeded", - }, - { - desc: "multiple short sleeps", - hook: "./testdata/sleepy.sh", - timeout: 1 * time.Second, - expected: "hook timed out", - }, - { - desc: "long sleep", - hook: "./testdata/sleeping_beauty.sh", - timeout: 1 * time.Second, - expected: "hook timed out", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - err := launchHook(test.hook, test.timeout, map[string]string{}) - require.EqualError(t, err, test.expected) - }) - } -} diff --git a/cmd/lego/main.go b/cmd/lego/main.go index c301a51f1..de4986993 100644 --- a/cmd/lego/main.go +++ b/cmd/lego/main.go @@ -13,6 +13,8 @@ import ( "github.com/urfave/cli/v2" ) +var version = "dev" + func main() { app := cli.NewApp() app.Name = "lego" @@ -20,13 +22,12 @@ func main() { app.Usage = "Let's Encrypt client written in Go" app.EnableBashCompletion = true - app.Version = getVersion() + app.Version = version cli.VersionPrinter = func(c *cli.Context) { fmt.Printf("lego version %s %s/%s\n", c.App.Version, runtime.GOOS, runtime.GOARCH) } var defaultPath string - cwd, err := os.Getwd() if err == nil { defaultPath = filepath.Join(cwd, ".lego") diff --git a/cmd/lego/zz_gen_version.go b/cmd/lego/zz_gen_version.go deleted file mode 100644 index cf9ad00ef..000000000 --- a/cmd/lego/zz_gen_version.go +++ /dev/null @@ -1,15 +0,0 @@ -// Code generated by 'internal/releaser'; DO NOT EDIT. - -package main - -const defaultVersion = "v4.32.0+dev-detach" - -var version = "" - -func getVersion() string { - if version == "" { - return defaultVersion - } - - return version -} diff --git a/cmd/setup.go b/cmd/setup.go index 6d15adad3..e07a87800 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -1,38 +1,23 @@ package cmd import ( - "context" "crypto/x509" - "encoding/json" "encoding/pem" "fmt" - "io" - "net/http" "os" "strings" "time" - "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/registration" - "github.com/hashicorp/go-retryablehttp" "github.com/urfave/cli/v2" ) const filePerm os.FileMode = 0o600 -// setupClient creates a new client with challenge settings. -func setupClient(ctx *cli.Context, account *Account, keyType certcrypto.KeyType) *lego.Client { - client := newClient(ctx, account, keyType) - - setupChallenges(ctx, client) - - return client -} - -func setupAccount(ctx *cli.Context, accountsStorage *AccountsStorage) (*Account, certcrypto.KeyType) { +func setup(ctx *cli.Context, accountsStorage *AccountsStorage) (*Account, *lego.Client) { keyType := getKeyType(ctx) privateKey := accountsStorage.GetPrivateKey(keyType) @@ -40,56 +25,35 @@ func setupAccount(ctx *cli.Context, accountsStorage *AccountsStorage) (*Account, if accountsStorage.ExistsAccountFilePath() { account = accountsStorage.LoadAccount(privateKey) } else { - account = &Account{Email: accountsStorage.GetEmail(), key: privateKey} + account = &Account{Email: accountsStorage.GetUserID(), key: privateKey} } - return account, keyType + client := newClient(ctx, account, keyType) + + return account, client } func newClient(ctx *cli.Context, acc registration.User, keyType certcrypto.KeyType) *lego.Client { config := lego.NewConfig(acc) - config.CADirURL = ctx.String(flgServer) + config.CADirURL = ctx.String("server") config.Certificate = lego.CertificateConfig{ - KeyType: keyType, - Timeout: time.Duration(ctx.Int(flgCertTimeout)) * time.Second, - OverallRequestLimit: ctx.Int(flgOverallRequestLimit), - DisableCommonName: ctx.Bool(flgDisableCommonName), + KeyType: keyType, + Timeout: time.Duration(ctx.Int("cert.timeout")) * time.Second, } config.UserAgent = getUserAgent(ctx) - if ctx.IsSet(flgHTTPTimeout) { - config.HTTPClient.Timeout = time.Duration(ctx.Int(flgHTTPTimeout)) * time.Second + if ctx.IsSet("http-timeout") { + config.HTTPClient.Timeout = time.Duration(ctx.Int("http-timeout")) * time.Second } - if ctx.Bool(flgTLSSkipVerify) { - defaultTransport, ok := config.HTTPClient.Transport.(*http.Transport) - if ok { // This is always true because the default client used by the CLI defined the transport. - tr := defaultTransport.Clone() - tr.TLSClientConfig.InsecureSkipVerify = true - config.HTTPClient.Transport = tr - } - } - - retryClient := retryablehttp.NewClient() - retryClient.RetryMax = 5 - retryClient.HTTPClient = config.HTTPClient - retryClient.CheckRetry = checkRetry - retryClient.Logger = nil - - if _, v := os.LookupEnv("LEGO_DEBUG_ACME_HTTP_CLIENT"); v { - retryClient.Logger = log.Logger - } - - config.HTTPClient = retryClient.StandardClient() - client, err := lego.NewClient(config) if err != nil { log.Fatalf("Could not create client: %v", err) } - if client.GetExternalAccountRequired() && !ctx.IsSet(flgEAB) { - log.Fatalf("Server requires External Account Binding. Use --%s with --%s and --%s.", flgEAB, flgKID, flgHMAC) + if client.GetExternalAccountRequired() && !ctx.IsSet("eab") { + log.Fatal("Server requires External Account Binding. Use --eab with --kid and --hmac.") } return client @@ -97,7 +61,7 @@ func newClient(ctx *cli.Context, acc registration.User, keyType certcrypto.KeyTy // getKeyType the type from which private keys should be generated. func getKeyType(ctx *cli.Context) certcrypto.KeyType { - keyType := ctx.String(flgKeyType) + keyType := ctx.String("key-type") switch strings.ToUpper(keyType) { case "RSA2048": return certcrypto.RSA2048 @@ -114,12 +78,19 @@ func getKeyType(ctx *cli.Context) certcrypto.KeyType { } log.Fatalf("Unsupported KeyType: %s", keyType) - return "" } +func getEmail(ctx *cli.Context) string { + email := ctx.String("email") + if email == "" { + log.Fatal("You have to pass an account (email address) to the program using --email or -m") + } + return email +} + func getUserAgent(ctx *cli.Context) string { - return strings.TrimSpace(fmt.Sprintf("%s lego-cli/%s", ctx.String(flgUserAgent), ctx.App.Version)) + return strings.TrimSpace(fmt.Sprintf("%s lego-cli/%s", ctx.String("user-agent"), ctx.App.Version)) } func createNonExistingFolder(path string) error { @@ -128,7 +99,6 @@ func createNonExistingFolder(path string) error { } else if err != nil { return err } - return nil } @@ -137,12 +107,10 @@ func readCSRFile(filename string) (*x509.CertificateRequest, error) { if err != nil { return nil, err } - raw := bytes // see if we can find a PEM-encoded CSR var p *pem.Block - rest := bytes for { // decode a PEM block @@ -164,49 +132,3 @@ func readCSRFile(filename string) (*x509.CertificateRequest, error) { // (if this assumption is wrong, parsing these bytes will fail) return x509.ParseCertificateRequest(raw) } - -func checkRetry(ctx context.Context, resp *http.Response, err error) (bool, error) { - rt, err := retryablehttp.ErrorPropagatedRetryPolicy(ctx, resp, err) - if err != nil { - return rt, err - } - - if resp == nil { - return rt, nil - } - - if resp.StatusCode/100 == 2 { - return rt, nil - } - - all, err := io.ReadAll(resp.Body) - if err == nil { - var errorDetails *acme.ProblemDetails - - err = json.Unmarshal(all, &errorDetails) - if err != nil { - return rt, fmt.Errorf("%s %s: %s", resp.Request.Method, resp.Request.URL.Redacted(), string(all)) - } - - switch errorDetails.Type { - case acme.BadNonceErr: - return false, &acme.NonceError{ - ProblemDetails: errorDetails, - } - - case acme.AlreadyReplacedErr: - if errorDetails.HTTPStatus == http.StatusConflict { - return false, &acme.AlreadyReplacedError{ - ProblemDetails: errorDetails, - } - } - - default: - log.Warnf("retry: %v", errorDetails) - - return rt, errorDetails - } - } - - return rt, nil -} diff --git a/cmd/setup_challenges.go b/cmd/setup_challenges.go index 6968c7ba3..719f8dd6c 100644 --- a/cmd/setup_challenges.go +++ b/cmd/setup_challenges.go @@ -1,7 +1,6 @@ package cmd import ( - "fmt" "net" "strings" "time" @@ -20,60 +19,54 @@ import ( ) func setupChallenges(ctx *cli.Context, client *lego.Client) { - if !ctx.Bool(flgHTTP) && !ctx.Bool(flgTLS) && !ctx.IsSet(flgDNS) { - log.Fatalf("No challenge selected. You must specify at least one challenge: `--%s`, `--%s`, `--%s`.", flgHTTP, flgTLS, flgDNS) + if !ctx.Bool("http") && !ctx.Bool("tls") && !ctx.IsSet("dns") { + log.Fatal("No challenge selected. You must specify at least one challenge: `--http`, `--tls`, `--dns`.") } - if ctx.Bool(flgHTTP) { - err := client.Challenge.SetHTTP01Provider(setupHTTPProvider(ctx), http01.SetDelay(ctx.Duration(flgHTTPDelay))) + if ctx.Bool("http") { + err := client.Challenge.SetHTTP01Provider(setupHTTPProvider(ctx)) if err != nil { log.Fatal(err) } } - if ctx.Bool(flgTLS) { - err := client.Challenge.SetTLSALPN01Provider(setupTLSProvider(ctx), tlsalpn01.SetDelay(ctx.Duration(flgTLSDelay))) + if ctx.Bool("tls") { + err := client.Challenge.SetTLSALPN01Provider(setupTLSProvider(ctx)) if err != nil { log.Fatal(err) } } - if ctx.IsSet(flgDNS) { - err := setupDNS(ctx, client) - if err != nil { - log.Fatal(err) - } + if ctx.IsSet("dns") { + setupDNS(ctx, client) } } //nolint:gocyclo // the complexity is expected. func setupHTTPProvider(ctx *cli.Context) challenge.Provider { switch { - case ctx.IsSet(flgHTTPWebroot): - ps, err := webroot.NewHTTPProvider(ctx.String(flgHTTPWebroot)) + case ctx.IsSet("http.webroot"): + ps, err := webroot.NewHTTPProvider(ctx.String("http.webroot")) if err != nil { log.Fatal(err) } - return ps - case ctx.IsSet(flgHTTPMemcachedHost): - ps, err := memcached.NewMemcachedProvider(ctx.StringSlice(flgHTTPMemcachedHost)) + case ctx.IsSet("http.memcached-host"): + ps, err := memcached.NewMemcachedProvider(ctx.StringSlice("http.memcached-host")) if err != nil { log.Fatal(err) } - return ps - case ctx.IsSet(flgHTTPS3Bucket): - ps, err := s3.NewHTTPProvider(ctx.String(flgHTTPS3Bucket)) + case ctx.IsSet("http.s3-bucket"): + ps, err := s3.NewHTTPProvider(ctx.String("http.s3-bucket")) if err != nil { log.Fatal(err) } - return ps - case ctx.IsSet(flgHTTPPort): - iface := ctx.String(flgHTTPPort) + case ctx.IsSet("http.port"): + iface := ctx.String("http.port") if !strings.Contains(iface, ":") { - log.Fatalf("The --%s switch only accepts interface:port or :port for its argument.", flgHTTPPort) + log.Fatalf("The --http switch only accepts interface:port or :port for its argument.") } host, port, err := net.SplitHostPort(iface) @@ -82,17 +75,15 @@ func setupHTTPProvider(ctx *cli.Context) challenge.Provider { } srv := http01.NewProviderServer(host, port) - if header := ctx.String(flgHTTPProxyHeader); header != "" { + if header := ctx.String("http.proxy-header"); header != "" { srv.SetProxyHeader(header) } - return srv - case ctx.Bool(flgHTTP): + case ctx.Bool("http"): srv := http01.NewProviderServer("", "") - if header := ctx.String(flgHTTPProxyHeader); header != "" { + if header := ctx.String("http.proxy-header"); header != "" { srv.SetProxyHeader(header) } - return srv default: log.Fatal("Invalid HTTP challenge options.") @@ -102,10 +93,10 @@ func setupHTTPProvider(ctx *cli.Context) challenge.Provider { func setupTLSProvider(ctx *cli.Context) challenge.Provider { switch { - case ctx.IsSet(flgTLSPort): - iface := ctx.String(flgTLSPort) + case ctx.IsSet("tls.port"): + iface := ctx.String("tls.port") if !strings.Contains(iface, ":") { - log.Fatalf("The --%s switch only accepts interface:port or :port for its argument.", flgTLSPort) + log.Fatalf("The --tls switch only accepts interface:port or :port for its argument.") } host, port, err := net.SplitHostPort(iface) @@ -114,7 +105,7 @@ func setupTLSProvider(ctx *cli.Context) challenge.Provider { } return tlsalpn01.NewProviderServer(host, port) - case ctx.Bool(flgTLS): + case ctx.Bool("tls"): return tlsalpn01.NewProviderServer("", "") default: log.Fatal("Invalid HTTP challenge options.") @@ -122,62 +113,22 @@ func setupTLSProvider(ctx *cli.Context) challenge.Provider { } } -func setupDNS(ctx *cli.Context, client *lego.Client) error { - err := checkPropagationExclusiveOptions(ctx) +func setupDNS(ctx *cli.Context, client *lego.Client) { + provider, err := dns.NewDNSChallengeProviderByName(ctx.String("dns")) if err != nil { - return err + log.Fatal(err) } - wait := ctx.Duration(flgDNSPropagationWait) - if wait < 0 { - return fmt.Errorf("'%s' cannot be negative", flgDNSPropagationWait) - } - - provider, err := dns.NewDNSChallengeProviderByName(ctx.String(flgDNS)) - if err != nil { - return err - } - - servers := ctx.StringSlice(flgDNSResolvers) - + servers := ctx.StringSlice("dns.resolvers") err = client.Challenge.SetDNS01Provider(provider, dns01.CondOption(len(servers) > 0, - dns01.AddRecursiveNameservers(dns01.ParseNameservers(ctx.StringSlice(flgDNSResolvers)))), - - dns01.CondOption(ctx.Bool(flgDNSDisableCP) || ctx.Bool(flgDNSPropagationDisableANS), - dns01.DisableAuthoritativeNssPropagationRequirement()), - - dns01.CondOption(ctx.Duration(flgDNSPropagationWait) > 0, - // TODO(ldez): inside the next major version we will use flgDNSDisableCP here. - // This will change the meaning of this flag to really disable all propagation checks. - dns01.PropagationWait(wait, true)), - - dns01.CondOption(ctx.Bool(flgDNSPropagationRNS), - dns01.RecursiveNSsPropagationRequirement()), - - dns01.CondOption(ctx.IsSet(flgDNSTimeout), - dns01.AddDNSTimeout(time.Duration(ctx.Int(flgDNSTimeout))*time.Second)), + dns01.AddRecursiveNameservers(dns01.ParseNameservers(ctx.StringSlice("dns.resolvers")))), + dns01.CondOption(ctx.Bool("dns.disable-cp"), + dns01.DisableCompletePropagationRequirement()), + dns01.CondOption(ctx.IsSet("dns-timeout"), + dns01.AddDNSTimeout(time.Duration(ctx.Int("dns-timeout"))*time.Second)), ) - - return err -} - -func checkPropagationExclusiveOptions(ctx *cli.Context) error { - if ctx.IsSet(flgDNSDisableCP) { - log.Printf("The flag '%s' is deprecated use '%s' instead.", flgDNSDisableCP, flgDNSPropagationDisableANS) + if err != nil { + log.Fatal(err) } - - if (isSetBool(ctx, flgDNSDisableCP) || isSetBool(ctx, flgDNSPropagationDisableANS)) && ctx.IsSet(flgDNSPropagationWait) { - return fmt.Errorf("'%s' and '%s' are mutually exclusive", flgDNSPropagationDisableANS, flgDNSPropagationWait) - } - - if isSetBool(ctx, flgDNSPropagationRNS) && ctx.IsSet(flgDNSPropagationWait) { - return fmt.Errorf("'%s' and '%s' are mutually exclusive", flgDNSPropagationRNS, flgDNSPropagationWait) - } - - return nil -} - -func isSetBool(ctx *cli.Context, name string) bool { - return ctx.IsSet(name) && ctx.Bool(name) } diff --git a/cmd/testdata/sleeping_beauty.sh b/cmd/testdata/sleeping_beauty.sh deleted file mode 100755 index 96b42a005..000000000 --- a/cmd/testdata/sleeping_beauty.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -e - -sleep 50 diff --git a/cmd/testdata/sleepy.sh b/cmd/testdata/sleepy.sh deleted file mode 100755 index 60bb903a1..000000000 --- a/cmd/testdata/sleepy.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -e - -for i in `seq 1 10` -do - echo $i - sleep 0.2 -done diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index f73f3920b..0e12e9c88 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -1,7 +1,8 @@ -// Code generated by 'make generate-dns'; DO NOT EDIT. - package cmd +// CODE GENERATED AUTOMATICALLY +// THIS FILE MUST NOT BE EDITED BY HAND + import ( "fmt" "io" @@ -12,28 +13,17 @@ import ( func allDNSCodes() string { providers := []string{ + "manual", "acme-dns", - "active24", "alidns", - "aliesa", "allinkl", - "alwaysdata", - "anexia", - "artfiles", "arvancloud", "auroradns", "autodns", - "axelname", - "azion", "azure", "azuredns", - "baiducloud", - "beget", - "binarylane", "bindman", "bluecat", - "bluecatv2", - "bookmyname", "brandit", "bunny", "checkdomain", @@ -43,20 +33,13 @@ func allDNSCodes() string { "cloudns", "cloudru", "cloudxns", - "com35", "conoha", - "conohav3", "constellix", - "corenetworks", "cpanel", - "czechia", - "ddnss", "derak", "desec", "designate", "digitalocean", - "directadmin", - "dnsexit", "dnshomede", "dnsimple", "dnsmadeeasy", @@ -66,37 +49,26 @@ func allDNSCodes() string { "dreamhost", "duckdns", "dyn", - "dyndnsfree", "dynu", "easydns", - "edgecenter", "edgedns", - "edgeone", "efficientip", "epik", - "eurodns", - "excedo", "exec", "exoscale", - "f5xc", "freemyip", "gandi", "gandiv5", "gcloud", "gcore", - "gigahostno", "glesys", "godaddy", "googledomains", - "gravity", "hetzner", "hostingde", - "hostinger", - "hostingnl", "hosttech", "httpnet", "httpreq", - "huaweicloud", "hurricane", "hyperone", "ibmcloud", @@ -107,47 +79,30 @@ func allDNSCodes() string { "internetbs", "inwx", "ionos", - "ionoscloud", "ipv64", - "ispconfig", - "ispconfigddns", "iwantmyname", - "jdcloud", "joker", - "keyhelp", - "leaseweb", "liara", "lightsail", - "limacity", "linode", "liquidweb", "loopia", "luadns", "mailinabox", - "manageengine", - "manual", "metaname", - "metaregistrar", - "mijnhost", - "mittwald", - "myaddr", "mydnsjp", "mythicbeasts", "namecheap", "namedotcom", "namesilo", - "namesurfer", "nearlyfreespeech", - "neodigit", "netcup", "netlify", "nicmanager", - "nicru", "nifcloud", "njalla", "nodion", "ns1", - "octenium", "oraclecloud", "otc", "ovh", @@ -155,9 +110,7 @@ func allDNSCodes() string { "plesk", "porkbun", "rackspace", - "rainyun", "rcodezero", - "regfish", "regru", "rfc2136", "rimuhosting", @@ -167,40 +120,28 @@ func allDNSCodes() string { "scaleway", "selectel", "selectelv2", - "selfhostde", "servercow", "shellrent", "simply", "sonic", - "spaceship", "stackpath", - "syse", - "technitium", "tencentcloud", - "timewebcloud", - "todaynic", "transip", "ultradns", - "uniteddomains", "variomedia", "vegadns", "vercel", "versio", "vinyldns", - "virtualname", "vkcloud", - "volcengine", "vscale", "vultr", "webnames", - "webnamesca", "websupport", "wedos", - "westcn", "yandex", "yandex360", "yandexcloud", - "zoneedit", "zoneee", "zonomi", } @@ -222,37 +163,12 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Credentials:`) ew.writeln(` - "ACME_DNS_API_BASE": The ACME-DNS API address`) - ew.writeln(` - "ACME_DNS_STORAGE_BASE_URL": The ACME-DNS JSON account data server.`) ew.writeln(` - "ACME_DNS_STORAGE_PATH": The ACME-DNS JSON account data file. A per-domain account will be registered/persisted to this file and used for TXT updates.`) ew.writeln() - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "ACME_DNS_ALLOWLIST": Source networks using CIDR notation (multiple values should be separated with a comma).`) - ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/acme-dns`) - case "active24": - // generated from: providers/dns/active24/active24.toml - ew.writeln(`Configuration for Active24.`) - ew.writeln(`Code: 'active24'`) - ew.writeln(`Since: 'v4.23.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "ACTIVE24_API_KEY": API key`) - ew.writeln(` - "ACTIVE24_SECRET": Secret`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "ACTIVE24_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "ACTIVE24_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "ACTIVE24_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "ACTIVE24_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/active24`) - case "alidns": // generated from: providers/dns/alidns/alidns.toml ew.writeln(`Configuration for Alibaba Cloud DNS.`) @@ -262,45 +178,20 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Credentials:`) ew.writeln(` - "ALICLOUD_ACCESS_KEY": Access key ID`) - ew.writeln(` - "ALICLOUD_RAM_ROLE": Your instance RAM role (https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance)`) + ew.writeln(` - "ALICLOUD_RAM_ROLE": Your instance RAM role (https://www.alibabacloud.com/help/doc-detail/54579.htm)`) ew.writeln(` - "ALICLOUD_SECRET_KEY": Access Key secret`) ew.writeln(` - "ALICLOUD_SECURITY_TOKEN": STS Security Token (optional)`) ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "ALICLOUD_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) - ew.writeln(` - "ALICLOUD_LINE": Line (Default: default)`) - ew.writeln(` - "ALICLOUD_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "ALICLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "ALICLOUD_REGION_ID": Region ID (Default: cn-hangzhou)`) - ew.writeln(` - "ALICLOUD_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`) + ew.writeln(` - "ALICLOUD_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "ALICLOUD_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "ALICLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "ALICLOUD_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/alidns`) - case "aliesa": - // generated from: providers/dns/aliesa/aliesa.toml - ew.writeln(`Configuration for AlibabaCloud ESA.`) - ew.writeln(`Code: 'aliesa'`) - ew.writeln(`Since: 'v4.29.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "ALIESA_ACCESS_KEY": Access key ID`) - ew.writeln(` - "ALIESA_RAM_ROLE": Your instance RAM role (https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance)`) - ew.writeln(` - "ALIESA_SECRET_KEY": Access Key secret`) - ew.writeln(` - "ALIESA_SECURITY_TOKEN": STS Security Token (optional)`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "ALIESA_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "ALIESA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "ALIESA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "ALIESA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/aliesa`) - case "allinkl": // generated from: providers/dns/allinkl/allinkl.toml ew.writeln(`Configuration for all-inkl.`) @@ -314,76 +205,13 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "ALL_INKL_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "ALL_INKL_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "ALL_INKL_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "ALL_INKL_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "ALL_INKL_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "ALL_INKL_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/allinkl`) - case "alwaysdata": - // generated from: providers/dns/alwaysdata/alwaysdata.toml - ew.writeln(`Configuration for Alwaysdata.`) - ew.writeln(`Code: 'alwaysdata'`) - ew.writeln(`Since: 'v4.31.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "ALWAYSDATA_API_KEY": API Key`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "ALWAYSDATA_ACCOUNT": Account name`) - ew.writeln(` - "ALWAYSDATA_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "ALWAYSDATA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "ALWAYSDATA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "ALWAYSDATA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/alwaysdata`) - - case "anexia": - // generated from: providers/dns/anexia/anexia.toml - ew.writeln(`Configuration for Anexia CloudDNS.`) - ew.writeln(`Code: 'anexia'`) - ew.writeln(`Since: 'v4.28.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "ANEXIA_TOKEN": API token for Anexia Engine`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "ANEXIA_API_URL": API endpoint URL (default: https://engine.anexia-it.com)`) - ew.writeln(` - "ANEXIA_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "ANEXIA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "ANEXIA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 300)`) - ew.writeln(` - "ANEXIA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/anexia`) - - case "artfiles": - // generated from: providers/dns/artfiles/artfiles.toml - ew.writeln(`Configuration for ArtFiles.`) - ew.writeln(`Code: 'artfiles'`) - ew.writeln(`Since: 'v4.32.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "ARTFILES_PASSWORD": API password`) - ew.writeln(` - "ARTFILES_USERNAME": API username`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "ARTFILES_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "ARTFILES_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "ARTFILES_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 360)`) - ew.writeln(` - "ARTFILES_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/artfiles`) - case "arvancloud": // generated from: providers/dns/arvancloud/arvancloud.toml ew.writeln(`Configuration for ArvanCloud.`) @@ -396,10 +224,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "ARVANCLOUD_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "ARVANCLOUD_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "ARVANCLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "ARVANCLOUD_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`) + ew.writeln(` - "ARVANCLOUD_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "ARVANCLOUD_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "ARVANCLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "ARVANCLOUD_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/arvancloud`) @@ -418,9 +246,9 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "AURORA_ENDPOINT": API endpoint URL`) - ew.writeln(` - "AURORA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "AURORA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "AURORA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "AURORA_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "AURORA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "AURORA_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/auroradns`) @@ -440,56 +268,14 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "AUTODNS_CONTEXT": API context (4 for production, 1 for testing. Defaults to 4)`) ew.writeln(` - "AUTODNS_ENDPOINT": API endpoint URL, defaults to https://api.autodns.com/v1/`) - ew.writeln(` - "AUTODNS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "AUTODNS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "AUTODNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "AUTODNS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`) + ew.writeln(` - "AUTODNS_HTTP_TIMEOUT": API request timeout, defaults to 30 seconds`) + ew.writeln(` - "AUTODNS_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "AUTODNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "AUTODNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/autodns`) - case "axelname": - // generated from: providers/dns/axelname/axelname.toml - ew.writeln(`Configuration for Axelname.`) - ew.writeln(`Code: 'axelname'`) - ew.writeln(`Since: 'v4.23.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "AXELNAME_NICKNAME": Account nickname`) - ew.writeln(` - "AXELNAME_TOKEN": API token`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "AXELNAME_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "AXELNAME_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "AXELNAME_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "AXELNAME_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/axelname`) - - case "azion": - // generated from: providers/dns/azion/azion.toml - ew.writeln(`Configuration for Azion.`) - ew.writeln(`Code: 'azion'`) - ew.writeln(`Since: 'v4.24.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "AZION_PERSONAL_TOKEN": Your Azion personal token.`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "AZION_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "AZION_PAGE_SIZE": The page size for the API request (Default: 50)`) - ew.writeln(` - "AZION_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "AZION_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "AZION_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/azion`) - case "azure": // generated from: providers/dns/azure/azure.toml ew.writeln(`Configuration for Azure (deprecated).`) @@ -509,10 +295,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "AZURE_METADATA_ENDPOINT": Metadata Service endpoint URL`) - ew.writeln(` - "AZURE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "AZURE_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "AZURE_PRIVATE_ZONE": Set to true to use Azure Private DNS Zones and not public`) - ew.writeln(` - "AZURE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "AZURE_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) + ew.writeln(` - "AZURE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "AZURE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln(` - "AZURE_ZONE_NAME": Zone name to use inside Azure DNS service to add the TXT record in`) ew.writeln() @@ -536,79 +322,18 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(` - "AZURE_AUTH_METHOD": Specify which authentication method to use`) ew.writeln(` - "AZURE_AUTH_MSI_TIMEOUT": Managed Identity timeout duration`) ew.writeln(` - "AZURE_ENVIRONMENT": Azure environment, one of: public, usgovernment, and china`) - ew.writeln(` - "AZURE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "AZURE_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "AZURE_PRIVATE_ZONE": Set to true to use Azure Private DNS Zones and not public`) - ew.writeln(` - "AZURE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) + ew.writeln(` - "AZURE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "AZURE_RESOURCE_GROUP": DNS zone resource group`) ew.writeln(` - "AZURE_SERVICEDISCOVERY_FILTER": Advanced ServiceDiscovery filter using Kusto query condition`) ew.writeln(` - "AZURE_SUBSCRIPTION_ID": DNS zone subscription ID`) - ew.writeln(` - "AZURE_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) + ew.writeln(` - "AZURE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln(` - "AZURE_ZONE_NAME": Zone name to use inside Azure DNS service to add the TXT record in`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/azuredns`) - case "baiducloud": - // generated from: providers/dns/baiducloud/baiducloud.toml - ew.writeln(`Configuration for Baidu Cloud.`) - ew.writeln(`Code: 'baiducloud'`) - ew.writeln(`Since: 'v4.23.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "BAIDUCLOUD_ACCESS_KEY_ID": Access key`) - ew.writeln(` - "BAIDUCLOUD_SECRET_ACCESS_KEY": Secret access key`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "BAIDUCLOUD_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "BAIDUCLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "BAIDUCLOUD_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/baiducloud`) - - case "beget": - // generated from: providers/dns/beget/beget.toml - ew.writeln(`Configuration for Beget.com.`) - ew.writeln(`Code: 'beget'`) - ew.writeln(`Since: 'v4.27.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "BEGET_PASSWORD": API password`) - ew.writeln(` - "BEGET_USERNAME": API username`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "BEGET_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "BEGET_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 30)`) - ew.writeln(` - "BEGET_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 300)`) - ew.writeln(` - "BEGET_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/beget`) - - case "binarylane": - // generated from: providers/dns/binarylane/binarylane.toml - ew.writeln(`Configuration for Binary Lane.`) - ew.writeln(`Code: 'binarylane'`) - ew.writeln(`Since: 'v4.26.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "BINARYLANE_API_TOKEN": API token`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "BINARYLANE_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "BINARYLANE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "BINARYLANE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "BINARYLANE_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/binarylane`) - case "bindman": // generated from: providers/dns/bindman/bindman.toml ew.writeln(`Configuration for Bindman.`) @@ -621,9 +346,9 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "BINDMAN_HTTP_TIMEOUT": API request timeout in seconds (Default: 60)`) - ew.writeln(` - "BINDMAN_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "BINDMAN_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "BINDMAN_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "BINDMAN_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "BINDMAN_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/bindman`) @@ -644,64 +369,17 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "BLUECAT_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "BLUECAT_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "BLUECAT_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "BLUECAT_SKIP_DEPLOY": Skip deployements`) - ew.writeln(` - "BLUECAT_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "BLUECAT_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "BLUECAT_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "BLUECAT_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "BLUECAT_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/bluecat`) - case "bluecatv2": - // generated from: providers/dns/bluecatv2/bluecatv2.toml - ew.writeln(`Configuration for Bluecat v2.`) - ew.writeln(`Code: 'bluecatv2'`) - ew.writeln(`Since: 'v4.32.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "BLUECATV2_CONFIG_NAME": Configuration name`) - ew.writeln(` - "BLUECATV2_PASSWORD": API password`) - ew.writeln(` - "BLUECATV2_USERNAME": API username`) - ew.writeln(` - "BLUECATV2_VIEW_NAME": DNS View Name`) - ew.writeln(` - "BLUECAT_SERVER_URL": The server URL: it should have a scheme, hostname, and port (if required) of the authoritative Bluecat BAM serve`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "BLUECATV2_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "BLUECATV2_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "BLUECATV2_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "BLUECATV2_SKIP_DEPLOY": Skip quick deployements`) - ew.writeln(` - "BLUECATV2_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/bluecatv2`) - - case "bookmyname": - // generated from: providers/dns/bookmyname/bookmyname.toml - ew.writeln(`Configuration for BookMyName.`) - ew.writeln(`Code: 'bookmyname'`) - ew.writeln(`Since: 'v4.23.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "BOOKMYNAME_PASSWORD": Password`) - ew.writeln(` - "BOOKMYNAME_USERNAME": Username`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "BOOKMYNAME_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "BOOKMYNAME_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "BOOKMYNAME_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "BOOKMYNAME_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/bookmyname`) - case "brandit": // generated from: providers/dns/brandit/brandit.toml - ew.writeln(`Configuration for Brandit (deprecated).`) + ew.writeln(`Configuration for Brandit.`) ew.writeln(`Code: 'brandit'`) ew.writeln(`Since: 'v4.11.0'`) ew.writeln() @@ -712,10 +390,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "BRANDIT_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "BRANDIT_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "BRANDIT_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 600)`) - ew.writeln(` - "BRANDIT_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`) + ew.writeln(` - "BRANDIT_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "BRANDIT_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "BRANDIT_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "BRANDIT_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/brandit`) @@ -732,10 +410,9 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "BUNNY_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "BUNNY_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "BUNNY_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "BUNNY_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) + ew.writeln(` - "BUNNY_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "BUNNY_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "BUNNY_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/bunny`) @@ -753,10 +430,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "CHECKDOMAIN_ENDPOINT": API endpoint URL, defaults to https://api.checkdomain.de`) - ew.writeln(` - "CHECKDOMAIN_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "CHECKDOMAIN_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 300)`) - ew.writeln(` - "CHECKDOMAIN_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 7)`) - ew.writeln(` - "CHECKDOMAIN_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "CHECKDOMAIN_HTTP_TIMEOUT": API request timeout, defaults to 30 seconds`) + ew.writeln(` - "CHECKDOMAIN_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "CHECKDOMAIN_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "CHECKDOMAIN_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/checkdomain`) @@ -773,9 +450,9 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "CIVO_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 30)`) - ew.writeln(` - "CIVO_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 300)`) - ew.writeln(` - "CIVO_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`) + ew.writeln(` - "CIVO_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "CIVO_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "CIVO_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/civo`) @@ -794,10 +471,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "CLOUDDNS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "CLOUDDNS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 5)`) - ew.writeln(` - "CLOUDDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "CLOUDDNS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "CLOUDDNS_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "CLOUDDNS_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "CLOUDDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "CLOUDDNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/clouddns`) @@ -821,11 +498,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "CLOUDFLARE_BASE_URL": API base URL (Default: https://api.cloudflare.com/client/v4)`) - ew.writeln(` - "CLOUDFLARE_HTTP_TIMEOUT": API request timeout in seconds (Default: )`) - ew.writeln(` - "CLOUDFLARE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "CLOUDFLARE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "CLOUDFLARE_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "CLOUDFLARE_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "CLOUDFLARE_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "CLOUDFLARE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "CLOUDFLARE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/cloudflare`) @@ -843,11 +519,11 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "CLOUDNS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "CLOUDNS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) - ew.writeln(` - "CLOUDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 180)`) + ew.writeln(` - "CLOUDNS_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "CLOUDNS_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "CLOUDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "CLOUDNS_SUB_AUTH_ID": The API sub user ID`) - ew.writeln(` - "CLOUDNS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) + ew.writeln(` - "CLOUDNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/cloudns`) @@ -866,18 +542,18 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "CLOUDRU_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "CLOUDRU_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 5)`) - ew.writeln(` - "CLOUDRU_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 300)`) - ew.writeln(` - "CLOUDRU_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 120)`) - ew.writeln(` - "CLOUDRU_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "CLOUDRU_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "CLOUDRU_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "CLOUDRU_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "CLOUDRU_SEQUENCE_INTERVAL": Time between sequential requests`) + ew.writeln(` - "CLOUDRU_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/cloudru`) case "cloudxns": // generated from: providers/dns/cloudxns/cloudxns.toml - ew.writeln(`Configuration for CloudXNS (Deprecated).`) + ew.writeln(`Configuration for CloudXNS.`) ew.writeln(`Code: 'cloudxns'`) ew.writeln(`Since: 'v0.5.0'`) ew.writeln() @@ -888,38 +564,17 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "CLOUDXNS_HTTP_TIMEOUT": API request timeout in seconds (Default: )`) - ew.writeln(` - "CLOUDXNS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: )`) - ew.writeln(` - "CLOUDXNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: )`) - ew.writeln(` - "CLOUDXNS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: )`) + ew.writeln(` - "CLOUDXNS_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "CLOUDXNS_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "CLOUDXNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "CLOUDXNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/cloudxns`) - case "com35": - // generated from: providers/dns/com35/com35.toml - ew.writeln(`Configuration for 35.com/三五互联.`) - ew.writeln(`Code: 'com35'`) - ew.writeln(`Since: 'v4.31.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "COM35_PASSWORD": API password`) - ew.writeln(` - "COM35_USERNAME": Username`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "COM35_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "COM35_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) - ew.writeln(` - "COM35_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "COM35_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/com35`) - case "conoha": // generated from: providers/dns/conoha/conoha.toml - ew.writeln(`Configuration for ConoHa v2.`) + ew.writeln(`Configuration for ConoHa.`) ew.writeln(`Code: 'conoha'`) ew.writeln(`Since: 'v1.2.0'`) ew.writeln() @@ -931,38 +586,15 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "CONOHA_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "CONOHA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "CONOHA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "CONOHA_REGION": The region (Default: tyo1)`) - ew.writeln(` - "CONOHA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) + ew.writeln(` - "CONOHA_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "CONOHA_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "CONOHA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "CONOHA_REGION": The region`) + ew.writeln(` - "CONOHA_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/conoha`) - case "conohav3": - // generated from: providers/dns/conohav3/conohav3.toml - ew.writeln(`Configuration for ConoHa v3.`) - ew.writeln(`Code: 'conohav3'`) - ew.writeln(`Since: 'v4.24.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "CONOHAV3_API_PASSWORD": The API password`) - ew.writeln(` - "CONOHAV3_API_USER_ID": The API user ID`) - ew.writeln(` - "CONOHAV3_TENANT_ID": Tenant ID`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "CONOHAV3_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "CONOHAV3_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "CONOHAV3_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "CONOHAV3_REGION": The region (Default: c3j1)`) - ew.writeln(` - "CONOHAV3_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/conohav3`) - case "constellix": // generated from: providers/dns/constellix/constellix.toml ew.writeln(`Configuration for Constellix.`) @@ -976,36 +608,14 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "CONSTELLIX_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "CONSTELLIX_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) - ew.writeln(` - "CONSTELLIX_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "CONSTELLIX_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) + ew.writeln(` - "CONSTELLIX_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "CONSTELLIX_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "CONSTELLIX_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "CONSTELLIX_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/constellix`) - case "corenetworks": - // generated from: providers/dns/corenetworks/corenetworks.toml - ew.writeln(`Configuration for Core-Networks.`) - ew.writeln(`Code: 'corenetworks'`) - ew.writeln(`Since: 'v4.20.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "CORENETWORKS_LOGIN": The username of the API account`) - ew.writeln(` - "CORENETWORKS_PASSWORD": The password`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "CORENETWORKS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "CORENETWORKS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "CORENETWORKS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "CORENETWORKS_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 60)`) - ew.writeln(` - "CORENETWORKS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/corenetworks`) - case "cpanel": // generated from: providers/dns/cpanel/cpanel.toml ew.writeln(`Configuration for CPanel/WHM.`) @@ -1020,56 +630,16 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "CPANEL_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "CPANEL_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "CPANEL_MODE": use cpanel API or WHM API (Default: cpanel)`) - ew.writeln(` - "CPANEL_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "CPANEL_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "CPANEL_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "CPANEL_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "CPANEL_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "CPANEL_REGION": The region`) + ew.writeln(` - "CPANEL_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/cpanel`) - case "czechia": - // generated from: providers/dns/czechia/czechia.toml - ew.writeln(`Configuration for Czechia.`) - ew.writeln(`Code: 'czechia'`) - ew.writeln(`Since: 'v4.33.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "CZECHIA_TOKEN": Authorization token`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "CZECHIA_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "CZECHIA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "CZECHIA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "CZECHIA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/czechia`) - - case "ddnss": - // generated from: providers/dns/ddnss/ddnss.toml - ew.writeln(`Configuration for DDnss (DynDNS Service).`) - ew.writeln(`Code: 'ddnss'`) - ew.writeln(`Since: 'v4.32.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "DDNSS_KEY": Update key`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "DDNSS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "DDNSS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "DDNSS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "DDNSS_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 60)`) - ew.writeln(` - "DDNSS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/ddnss`) - case "derak": // generated from: providers/dns/derak/derak.toml ew.writeln(`Configuration for Derak Cloud.`) @@ -1082,10 +652,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "DERAK_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "DERAK_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 5)`) - ew.writeln(` - "DERAK_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "DERAK_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "DERAK_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "DERAK_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "DERAK_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "DERAK_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln(` - "DERAK_WEBSITE_ID": Force the zone/website ID`) ew.writeln() @@ -1103,10 +673,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "DESEC_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "DESEC_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 4)`) - ew.writeln(` - "DESEC_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "DESEC_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`) + ew.writeln(` - "DESEC_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "DESEC_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "DESEC_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "DESEC_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/desec`) @@ -1131,10 +701,9 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "DESIGNATE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) - ew.writeln(` - "DESIGNATE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 600)`) - ew.writeln(` - "DESIGNATE_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 10)`) - ew.writeln(` - "DESIGNATE_ZONE_NAME": The zone name to use in the OpenStack Project to manage TXT records.`) + ew.writeln(` - "DESIGNATE_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "DESIGNATE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "DESIGNATE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln(` - "OS_PROJECT_ID": Project ID`) ew.writeln(` - "OS_TENANT_NAME": Tenant name (deprecated see OS_PROJECT_NAME and OS_PROJECT_ID)`) @@ -1154,57 +723,14 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "DO_API_URL": The URL of the API`) - ew.writeln(` - "DO_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "DO_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 5)`) - ew.writeln(` - "DO_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "DO_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 30)`) + ew.writeln(` - "DO_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "DO_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "DO_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "DO_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/digitalocean`) - case "directadmin": - // generated from: providers/dns/directadmin/directadmin.toml - ew.writeln(`Configuration for DirectAdmin.`) - ew.writeln(`Code: 'directadmin'`) - ew.writeln(`Since: 'v4.18.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "DIRECTADMIN_API_URL": URL of the API`) - ew.writeln(` - "DIRECTADMIN_PASSWORD": API password`) - ew.writeln(` - "DIRECTADMIN_USERNAME": API username`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "DIRECTADMIN_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "DIRECTADMIN_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 5)`) - ew.writeln(` - "DIRECTADMIN_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "DIRECTADMIN_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 30)`) - ew.writeln(` - "DIRECTADMIN_ZONE_NAME": Zone name used to add the TXT record`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/directadmin`) - - case "dnsexit": - // generated from: providers/dns/dnsexit/dnsexit.toml - ew.writeln(`Configuration for DNSExit.`) - ew.writeln(`Code: 'dnsexit'`) - ew.writeln(`Since: 'v4.32.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "DNSEXIT_API_KEY": API key`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "DNSEXIT_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "DNSEXIT_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) - ew.writeln(` - "DNSEXIT_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 300)`) - ew.writeln(` - "DNSEXIT_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/dnsexit`) - case "dnshomede": // generated from: providers/dns/dnshomede/dnshomede.toml ew.writeln(`Configuration for dnsHome.de.`) @@ -1216,12 +742,6 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(` - "DNSHOMEDE_CREDENTIALS": Comma-separated list of domain:password credential pairs`) ew.writeln() - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "DNSHOMEDE_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "DNSHOMEDE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 1200)`) - ew.writeln(` - "DNSHOMEDE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 2)`) - ew.writeln(` - "DNSHOMEDE_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 120)`) - ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/dnshomede`) @@ -1238,9 +758,9 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "DNSIMPLE_BASE_URL": API endpoint URL`) - ew.writeln(` - "DNSIMPLE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "DNSIMPLE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "DNSIMPLE_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "DNSIMPLE_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "DNSIMPLE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "DNSIMPLE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/dnsimple`) @@ -1258,11 +778,11 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "DNSMADEEASY_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) - ew.writeln(` - "DNSMADEEASY_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "DNSMADEEASY_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "DNSMADEEASY_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "DNSMADEEASY_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "DNSMADEEASY_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "DNSMADEEASY_SANDBOX": Activate the sandbox (boolean)`) - ew.writeln(` - "DNSMADEEASY_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "DNSMADEEASY_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/dnsmadeeasy`) @@ -1279,10 +799,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "DNSPOD_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "DNSPOD_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "DNSPOD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "DNSPOD_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`) + ew.writeln(` - "DNSPOD_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "DNSPOD_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "DNSPOD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "DNSPOD_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/dnspod`) @@ -1299,10 +819,11 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "DODE_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "DODE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "DODE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "DODE_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 60)`) + ew.writeln(` - "DODE_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "DODE_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "DODE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "DODE_SEQUENCE_INTERVAL": Time between sequential requests`) + ew.writeln(` - "DODE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/dode`) @@ -1320,9 +841,9 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "DOMENESHOP_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "DOMENESHOP_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 20)`) - ew.writeln(` - "DOMENESHOP_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 300)`) + ew.writeln(` - "DOMENESHOP_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "DOMENESHOP_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "DOMENESHOP_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/domeneshop`) @@ -1339,9 +860,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "DREAMHOST_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "DREAMHOST_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 60)`) - ew.writeln(` - "DREAMHOST_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 3600)`) + ew.writeln(` - "DREAMHOST_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "DREAMHOST_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "DREAMHOST_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "DREAMHOST_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/dreamhost`) @@ -1358,10 +880,11 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "DUCKDNS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "DUCKDNS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "DUCKDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "DUCKDNS_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 60)`) + ew.writeln(` - "DUCKDNS_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "DUCKDNS_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "DUCKDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "DUCKDNS_SEQUENCE_INTERVAL": Time between sequential requests`) + ew.writeln(` - "DUCKDNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/duckdns`) @@ -1380,34 +903,14 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "DYN_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) - ew.writeln(` - "DYN_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "DYN_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "DYN_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "DYN_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "DYN_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "DYN_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "DYN_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/dyn`) - case "dyndnsfree": - // generated from: providers/dns/dyndnsfree/dyndnsfree.toml - ew.writeln(`Configuration for DynDnsFree.de.`) - ew.writeln(`Code: 'dyndnsfree'`) - ew.writeln(`Since: 'v4.23.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "DYNDNSFREE_PASSWORD": Password`) - ew.writeln(` - "DYNDNSFREE_USERNAME": Username`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "DYNDNSFREE_HTTP_TIMEOUT": Request timeout in seconds (Default: 30)`) - ew.writeln(` - "DYNDNSFREE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "DYNDNSFREE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/dyndnsfree`) - case "dynu": // generated from: providers/dns/dynu/dynu.toml ew.writeln(`Configuration for Dynu.`) @@ -1420,10 +923,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "DYNU_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "DYNU_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) - ew.writeln(` - "DYNU_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 180)`) - ew.writeln(` - "DYNU_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "DYNU_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "DYNU_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "DYNU_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "DYNU_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/dynu`) @@ -1442,35 +945,15 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "EASYDNS_ENDPOINT": The endpoint URL of the API Server`) - ew.writeln(` - "EASYDNS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "EASYDNS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "EASYDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "EASYDNS_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 60)`) - ew.writeln(` - "EASYDNS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "EASYDNS_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "EASYDNS_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "EASYDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "EASYDNS_SEQUENCE_INTERVAL": Time between sequential requests`) + ew.writeln(` - "EASYDNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/easydns`) - case "edgecenter": - // generated from: providers/dns/edgecenter/edgecenter.toml - ew.writeln(`Configuration for EdgeCenter.`) - ew.writeln(`Code: 'edgecenter'`) - ew.writeln(`Since: 'v4.29.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "EDGECENTER_PERMANENT_API_TOKEN": Permanent API token (https://edgecenter.ru/blog/permanent-api-token-explained/)`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "EDGECENTER_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) - ew.writeln(` - "EDGECENTER_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 20)`) - ew.writeln(` - "EDGECENTER_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 360)`) - ew.writeln(` - "EDGECENTER_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/edgecenter`) - case "edgedns": // generated from: providers/dns/edgedns/edgedns.toml ew.writeln(`Configuration for Akamai EdgeDNS.`) @@ -1488,38 +971,13 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "AKAMAI_ACCOUNT_SWITCH_KEY": Target account ID when the DNS zone and credentials belong to different accounts`) - ew.writeln(` - "AKAMAI_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 15)`) - ew.writeln(` - "AKAMAI_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 180)`) - ew.writeln(` - "AKAMAI_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "AKAMAI_POLLING_INTERVAL": Time between DNS propagation check. Default: 15 seconds`) + ew.writeln(` - "AKAMAI_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation. Default: 3 minutes`) + ew.writeln(` - "AKAMAI_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/edgedns`) - case "edgeone": - // generated from: providers/dns/edgeone/edgeone.toml - ew.writeln(`Configuration for Tencent EdgeOne.`) - ew.writeln(`Code: 'edgeone'`) - ew.writeln(`Since: 'v4.26.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "EDGEONE_SECRET_ID": Access key ID`) - ew.writeln(` - "EDGEONE_SECRET_KEY": Access Key secret`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "EDGEONE_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "EDGEONE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 30)`) - ew.writeln(` - "EDGEONE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 1200)`) - ew.writeln(` - "EDGEONE_REGION": Region`) - ew.writeln(` - "EDGEONE_SESSION_TOKEN": Access Key token`) - ew.writeln(` - "EDGEONE_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) - ew.writeln(` - "EDGEONE_ZONES_MAPPING": Mapping between DNS zones and site IDs. (ex: 'example.org:id1,example.com:id2')`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/edgeone`) - case "efficientip": // generated from: providers/dns/efficientip/efficientip.toml ew.writeln(`Configuration for Efficient IP.`) @@ -1535,10 +993,11 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "EFFICIENTIP_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) + ew.writeln(` - "EFFICIENTIP_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "EFFICIENTIP_INSECURE_SKIP_VERIFY": Whether or not to verify EfficientIP API certificate`) - ew.writeln(` - "EFFICIENTIP_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "EFFICIENTIP_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "EFFICIENTIP_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "EFFICIENTIP_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "EFFICIENTIP_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln(` - "EFFICIENTIP_VIEW_NAME": View name (ex: external)`) ew.writeln() @@ -1556,56 +1015,14 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "EPIK_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "EPIK_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "EPIK_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "EPIK_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`) + ew.writeln(` - "EPIK_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "EPIK_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "EPIK_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "EPIK_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/epik`) - case "eurodns": - // generated from: providers/dns/eurodns/eurodns.toml - ew.writeln(`Configuration for EuroDNS.`) - ew.writeln(`Code: 'eurodns'`) - ew.writeln(`Since: 'v4.33.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "EURODNS_API_KEY": API key`) - ew.writeln(` - "EURODNS_APP_ID": Application ID`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "EURODNS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "EURODNS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "EURODNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "EURODNS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/eurodns`) - - case "excedo": - // generated from: providers/dns/excedo/excedo.toml - ew.writeln(`Configuration for Excedo.`) - ew.writeln(`Code: 'excedo'`) - ew.writeln(`Since: 'v4.33.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "EXCEDO_API_KEY": API key`) - ew.writeln(` - "EXCEDO_API_URL": API base URL`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "EXCEDO_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "EXCEDO_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) - ew.writeln(` - "EXCEDO_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 300)`) - ew.writeln(` - "EXCEDO_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/excedo`) - case "exec": // generated from: providers/dns/exec/exec.toml ew.writeln(`Configuration for External program.`) @@ -1629,38 +1046,16 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) + ew.writeln(` - "EXOSCALE_API_ZONE": API zone`) ew.writeln(` - "EXOSCALE_ENDPOINT": API endpoint URL`) - ew.writeln(` - "EXOSCALE_HTTP_TIMEOUT": API request timeout in seconds (Default: 60)`) - ew.writeln(` - "EXOSCALE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "EXOSCALE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "EXOSCALE_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "EXOSCALE_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "EXOSCALE_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "EXOSCALE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "EXOSCALE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/exoscale`) - case "f5xc": - // generated from: providers/dns/f5xc/f5xc.toml - ew.writeln(`Configuration for F5 XC.`) - ew.writeln(`Code: 'f5xc'`) - ew.writeln(`Since: 'v4.23.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "F5XC_API_TOKEN": API token`) - ew.writeln(` - "F5XC_GROUP_NAME": Group name`) - ew.writeln(` - "F5XC_TENANT_NAME": XC Tenant shortname`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "F5XC_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "F5XC_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "F5XC_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "F5XC_SERVER": Server domain (Default: console.ves.volterra.io)`) - ew.writeln(` - "F5XC_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/f5xc`) - case "freemyip": // generated from: providers/dns/freemyip/freemyip.toml ew.writeln(`Configuration for freemyip.com.`) @@ -1673,11 +1068,11 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "FREEMYIP_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "FREEMYIP_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "FREEMYIP_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "FREEMYIP_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 60)`) - ew.writeln(` - "FREEMYIP_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`) + ew.writeln(` - "FREEMYIP_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "FREEMYIP_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "FREEMYIP_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "FREEMYIP_SEQUENCE_INTERVAL": Time between sequential requests`) + ew.writeln(` - "FREEMYIP_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/freemyip`) @@ -1694,10 +1089,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "GANDI_HTTP_TIMEOUT": API request timeout in seconds (Default: 60)`) - ew.writeln(` - "GANDI_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 60)`) - ew.writeln(` - "GANDI_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 2400)`) - ew.writeln(` - "GANDI_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "GANDI_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "GANDI_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "GANDI_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "GANDI_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/gandi`) @@ -1715,10 +1110,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "GANDIV5_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) - ew.writeln(` - "GANDIV5_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 20)`) - ew.writeln(` - "GANDIV5_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 1200)`) - ew.writeln(` - "GANDIV5_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "GANDIV5_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "GANDIV5_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "GANDIV5_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "GANDIV5_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/gandiv5`) @@ -1739,10 +1134,9 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "GCE_ALLOW_PRIVATE_ZONE": Allows requested domain to be in private DNS zone, works only with a private ACME server (by default: false)`) - ew.writeln(` - "GCE_IMPERSONATE_SERVICE_ACCOUNT": Service account email to impersonate`) - ew.writeln(` - "GCE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 5)`) - ew.writeln(` - "GCE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 180)`) - ew.writeln(` - "GCE_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "GCE_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "GCE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "GCE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln(` - "GCE_ZONE_ID": Allows to skip the automatic detection of the zone`) ew.writeln() @@ -1760,36 +1154,14 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "GCORE_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) - ew.writeln(` - "GCORE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 20)`) - ew.writeln(` - "GCORE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 360)`) - ew.writeln(` - "GCORE_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "GCORE_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "GCORE_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "GCORE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "GCORE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/gcore`) - case "gigahostno": - // generated from: providers/dns/gigahostno/gigahostno.toml - ew.writeln(`Configuration for Gigahost.no.`) - ew.writeln(`Code: 'gigahostno'`) - ew.writeln(`Since: 'v4.29.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "GIGAHOSTNO_PASSWORD": Password`) - ew.writeln(` - "GIGAHOSTNO_USERNAME": Username`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "GIGAHOSTNO_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "GIGAHOSTNO_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "GIGAHOSTNO_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "GIGAHOSTNO_SECRET": TOTP secret`) - ew.writeln(` - "GIGAHOSTNO_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/gigahostno`) - case "glesys": // generated from: providers/dns/glesys/glesys.toml ew.writeln(`Configuration for Glesys.`) @@ -1803,10 +1175,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "GLESYS_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) - ew.writeln(` - "GLESYS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 20)`) - ew.writeln(` - "GLESYS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 1200)`) - ew.writeln(` - "GLESYS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) + ew.writeln(` - "GLESYS_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "GLESYS_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "GLESYS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "GLESYS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/glesys`) @@ -1824,10 +1196,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "GODADDY_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "GODADDY_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "GODADDY_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "GODADDY_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`) + ew.writeln(` - "GODADDY_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "GODADDY_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "GODADDY_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "GODADDY_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/godaddy`) @@ -1844,35 +1216,13 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "GOOGLE_DOMAINS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "GOOGLE_DOMAINS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "GOOGLE_DOMAINS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) + ew.writeln(` - "GOOGLE_DOMAINS_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "GOOGLE_DOMAINS_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "GOOGLE_DOMAINS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/googledomains`) - case "gravity": - // generated from: providers/dns/gravity/gravity.toml - ew.writeln(`Configuration for Gravity.`) - ew.writeln(`Code: 'gravity'`) - ew.writeln(`Since: 'v4.30.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "GRAVITY_PASSWORD": Password`) - ew.writeln(` - "GRAVITY_SERVER_URL": URL of the server`) - ew.writeln(` - "GRAVITY_USERNAME": Username`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "GRAVITY_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "GRAVITY_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "GRAVITY_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "GRAVITY_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 1)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/gravity`) - case "hetzner": // generated from: providers/dns/hetzner/hetzner.toml ew.writeln(`Configuration for Hetzner.`) @@ -1881,14 +1231,14 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Credentials:`) - ew.writeln(` - "HETZNER_API_TOKEN": API token`) + ew.writeln(` - "HETZNER_API_KEY": API key`) ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "HETZNER_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "HETZNER_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "HETZNER_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "HETZNER_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "HETZNER_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "HETZNER_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "HETZNER_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "HETZNER_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/hetzner`) @@ -1905,55 +1255,15 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "HOSTINGDE_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "HOSTINGDE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "HOSTINGDE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "HOSTINGDE_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "HOSTINGDE_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "HOSTINGDE_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "HOSTINGDE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "HOSTINGDE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln(` - "HOSTINGDE_ZONE_NAME": Zone name in ACE format`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/hostingde`) - case "hostinger": - // generated from: providers/dns/hostinger/hostinger.toml - ew.writeln(`Configuration for Hostinger.`) - ew.writeln(`Code: 'hostinger'`) - ew.writeln(`Since: 'v4.27.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "HOSTINGER_API_TOKEN": API Token`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "HOSTINGER_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "HOSTINGER_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "HOSTINGER_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "HOSTINGER_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/hostinger`) - - case "hostingnl": - // generated from: providers/dns/hostingnl/hostingnl.toml - ew.writeln(`Configuration for Hosting.nl.`) - ew.writeln(`Code: 'hostingnl'`) - ew.writeln(`Since: 'v4.30.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "HOSTINGNL_API_KEY": The API key`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "HOSTINGNL_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) - ew.writeln(` - "HOSTINGNL_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "HOSTINGNL_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "HOSTINGNL_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/hostingnl`) - case "hosttech": // generated from: providers/dns/hosttech/hosttech.toml ew.writeln(`Configuration for Hosttech.`) @@ -1967,10 +1277,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "HOSTTECH_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "HOSTTECH_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "HOSTTECH_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "HOSTTECH_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`) + ew.writeln(` - "HOSTTECH_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "HOSTTECH_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "HOSTTECH_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "HOSTTECH_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/hosttech`) @@ -1987,10 +1297,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "HTTPNET_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "HTTPNET_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "HTTPNET_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "HTTPNET_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "HTTPNET_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "HTTPNET_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "HTTPNET_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "HTTPNET_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln(` - "HTTPNET_ZONE_NAME": Zone name in ACE format`) ew.writeln() @@ -2009,37 +1319,15 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "HTTPREQ_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) + ew.writeln(` - "HTTPREQ_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "HTTPREQ_PASSWORD": Basic authentication password`) - ew.writeln(` - "HTTPREQ_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "HTTPREQ_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "HTTPREQ_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "HTTPREQ_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "HTTPREQ_USERNAME": Basic authentication username`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/httpreq`) - case "huaweicloud": - // generated from: providers/dns/huaweicloud/huaweicloud.toml - ew.writeln(`Configuration for Huawei Cloud.`) - ew.writeln(`Code: 'huaweicloud'`) - ew.writeln(`Since: 'v4.19'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "HUAWEICLOUD_ACCESS_KEY_ID": Access key ID`) - ew.writeln(` - "HUAWEICLOUD_REGION": Region`) - ew.writeln(` - "HUAWEICLOUD_SECRET_ACCESS_KEY": Access Key secret`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "HUAWEICLOUD_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "HUAWEICLOUD_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "HUAWEICLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "HUAWEICLOUD_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/huaweicloud`) - case "hurricane": // generated from: providers/dns/hurricane/hurricane.toml ew.writeln(`Configuration for Hurricane Electric DNS.`) @@ -2051,12 +1339,6 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(` - "HURRICANE_TOKENS": TXT record names and tokens`) ew.writeln() - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "HURRICANE_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "HURRICANE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "HURRICANE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation (Default: 300)`) - ew.writeln(` - "HURRICANE_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 60)`) - ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/hurricane`) @@ -2069,12 +1351,11 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "HYPERONE_API_URL": Allows to pass custom API Endpoint to be used in the challenge (default https://api.hyperone.com/v2)`) - ew.writeln(` - "HYPERONE_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) ew.writeln(` - "HYPERONE_LOCATION_ID": Specifies location (region) to be used in API calls. (default pl-waw-1)`) ew.writeln(` - "HYPERONE_PASSPORT_LOCATION": Allows to pass custom passport file location (default ~/.h1/passport.json)`) - ew.writeln(` - "HYPERONE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 60)`) - ew.writeln(` - "HYPERONE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 2)`) - ew.writeln(` - "HYPERONE_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "HYPERONE_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "HYPERONE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "HYPERONE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/hyperone`) @@ -2088,14 +1369,14 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Credentials:`) ew.writeln(` - "SOFTLAYER_API_KEY": Classic Infrastructure API key`) - ew.writeln(` - "SOFTLAYER_USERNAME": Username (IBM Cloud is {accountID}_{emailAddress})`) + ew.writeln(` - "SOFTLAYER_USERNAME": Username (IBM Cloud is _)`) ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "SOFTLAYER_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "SOFTLAYER_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "SOFTLAYER_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "SOFTLAYER_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "SOFTLAYER_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "SOFTLAYER_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "SOFTLAYER_TIMEOUT": API request timeout`) + ew.writeln(` - "SOFTLAYER_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/ibmcloud`) @@ -2114,9 +1395,9 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "IIJ_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 4)`) - ew.writeln(` - "IIJ_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 240)`) - ew.writeln(` - "IIJ_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "IIJ_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "IIJ_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "IIJ_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/iij`) @@ -2135,9 +1416,9 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "IIJ_DPF_API_ENDPOINT": API endpoint URL, defaults to https://api.dns-platform.jp/dpf/v1`) - ew.writeln(` - "IIJ_DPF_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 5)`) - ew.writeln(` - "IIJ_DPF_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 660)`) - ew.writeln(` - "IIJ_DPF_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "IIJ_DPF_POLLING_INTERVAL": Time between DNS propagation check, defaults to 5 second`) + ew.writeln(` - "IIJ_DPF_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation, defaults to 660 second`) + ew.writeln(` - "IIJ_DPF_TTL": The TTL of the TXT record used for the DNS challenge, default to 300`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/iijdpf`) @@ -2156,15 +1437,14 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "INFOBLOX_CA_CERTIFICATE": The path to the CA certificate (PEM encoded)`) - ew.writeln(` - "INFOBLOX_DNS_VIEW": The view for the TXT records (Default: External)`) - ew.writeln(` - "INFOBLOX_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "INFOBLOX_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "INFOBLOX_PORT": The port for the infoblox grid manager (Default: 443)`) - ew.writeln(` - "INFOBLOX_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "INFOBLOX_SSL_VERIFY": Whether or not to verify the TLS certificate (Default: true)`) - ew.writeln(` - "INFOBLOX_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - ew.writeln(` - "INFOBLOX_WAPI_VERSION": The version of WAPI being used (Default: 2.11)`) + ew.writeln(` - "INFOBLOX_DNS_VIEW": The view for the TXT records, default: External`) + ew.writeln(` - "INFOBLOX_HTTP_TIMEOUT": HTTP request timeout`) + ew.writeln(` - "INFOBLOX_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "INFOBLOX_PORT": The port for the infoblox grid manager, default: 443`) + ew.writeln(` - "INFOBLOX_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "INFOBLOX_SSL_VERIFY": Whether or not to verify the TLS certificate, default: true`) + ew.writeln(` - "INFOBLOX_TTL": The TTL of the TXT record used for the DNS challenge`) + ew.writeln(` - "INFOBLOX_WAPI_VERSION": The version of WAPI being used, default: 2.11`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/infoblox`) @@ -2182,10 +1462,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "INFOMANIAK_ENDPOINT": https://api.infomaniak.com`) - ew.writeln(` - "INFOMANIAK_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "INFOMANIAK_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) - ew.writeln(` - "INFOMANIAK_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "INFOMANIAK_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "INFOMANIAK_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "INFOMANIAK_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "INFOMANIAK_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "INFOMANIAK_TTL": The TTL of the TXT record used for the DNS challenge in seconds`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/infomaniak`) @@ -2203,10 +1483,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "INTERNET_BS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "INTERNET_BS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "INTERNET_BS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "INTERNET_BS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`) + ew.writeln(` - "INTERNET_BS_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "INTERNET_BS_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "INTERNET_BS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "INTERNET_BS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/internetbs`) @@ -2224,11 +1504,11 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "INWX_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "INWX_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 360)`) + ew.writeln(` - "INWX_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "INWX_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation (default 360s)`) ew.writeln(` - "INWX_SANDBOX": Activate the sandbox (boolean)`) ew.writeln(` - "INWX_SHARED_SECRET": shared secret related to 2FA`) - ew.writeln(` - "INWX_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "INWX_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/inwx`) @@ -2245,34 +1525,14 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "IONOS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "IONOS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "IONOS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 900)`) - ew.writeln(` - "IONOS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "IONOS_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "IONOS_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "IONOS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "IONOS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/ionos`) - case "ionoscloud": - // generated from: providers/dns/ionoscloud/ionoscloud.toml - ew.writeln(`Configuration for Ionos Cloud.`) - ew.writeln(`Code: 'ionoscloud'`) - ew.writeln(`Since: 'v4.30.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "IONOSCLOUD_API_TOKEN": API token`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "IONOSCLOUD_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "IONOSCLOUD_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "IONOSCLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "IONOSCLOUD_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/ionoscloud`) - case "ipv64": // generated from: providers/dns/ipv64/ipv64.toml ew.writeln(`Configuration for IPv64.`) @@ -2285,60 +1545,17 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "IPV64_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "IPV64_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "IPV64_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "IPV64_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "IPV64_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "IPV64_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "IPV64_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/ipv64`) - case "ispconfig": - // generated from: providers/dns/ispconfig/ispconfig.toml - ew.writeln(`Configuration for ISPConfig 3.`) - ew.writeln(`Code: 'ispconfig'`) - ew.writeln(`Since: 'v4.31.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "ISPCONFIG_PASSWORD": Password`) - ew.writeln(` - "ISPCONFIG_SERVER_URL": Server URL`) - ew.writeln(` - "ISPCONFIG_USERNAME": Username`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "ISPCONFIG_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "ISPCONFIG_INSECURE_SKIP_VERIFY": Whether to verify the API certificate`) - ew.writeln(` - "ISPCONFIG_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "ISPCONFIG_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "ISPCONFIG_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/ispconfig`) - - case "ispconfigddns": - // generated from: providers/dns/ispconfigddns/ispconfigddns.toml - ew.writeln(`Configuration for ISPConfig 3 - Dynamic DNS (DDNS) Module.`) - ew.writeln(`Code: 'ispconfigddns'`) - ew.writeln(`Since: 'v4.31.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "ISPCONFIG_DDNS_SERVER_URL": API server URL (ex: https://panel.example.com:8080)`) - ew.writeln(` - "ISPCONFIG_DDNS_TOKEN": DDNS API token`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "ISPCONFIG_DDNS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "ISPCONFIG_DDNS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "ISPCONFIG_DDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "ISPCONFIG_DDNS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/ispconfigddns`) - case "iwantmyname": // generated from: providers/dns/iwantmyname/iwantmyname.toml - ew.writeln(`Configuration for iwantmyname (Deprecated).`) + ew.writeln(`Configuration for iwantmyname.`) ew.writeln(`Code: 'iwantmyname'`) ew.writeln(`Since: 'v4.7.0'`) ew.writeln() @@ -2349,36 +1566,14 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "IWANTMYNAME_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "IWANTMYNAME_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "IWANTMYNAME_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "IWANTMYNAME_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "IWANTMYNAME_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "IWANTMYNAME_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "IWANTMYNAME_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "IWANTMYNAME_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/iwantmyname`) - case "jdcloud": - // generated from: providers/dns/jdcloud/jdcloud.toml - ew.writeln(`Configuration for JD Cloud.`) - ew.writeln(`Code: 'jdcloud'`) - ew.writeln(`Since: 'v4.31.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "JDCLOUD_ACCESS_KEY_ID": Access key ID`) - ew.writeln(` - "JDCLOUD_ACCESS_KEY_SECRET": Access key secret`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "JDCLOUD_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "JDCLOUD_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "JDCLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "JDCLOUD_REGION_ID": Region ID (Default: cn-north-1)`) - ew.writeln(` - "JDCLOUD_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/jdcloud`) - case "joker": // generated from: providers/dns/joker/joker.toml ew.writeln(`Configuration for Joker.`) @@ -2394,56 +1589,15 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "JOKER_HTTP_TIMEOUT": API request timeout in seconds (Default: 60)`) - ew.writeln(` - "JOKER_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "JOKER_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "JOKER_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 60), only with 'SVC' mode`) - ew.writeln(` - "JOKER_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "JOKER_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "JOKER_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "JOKER_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "JOKER_SEQUENCE_INTERVAL": Time between sequential requests (only with 'SVC' mode)`) + ew.writeln(` - "JOKER_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/joker`) - case "keyhelp": - // generated from: providers/dns/keyhelp/keyhelp.toml - ew.writeln(`Configuration for KeyHelp.`) - ew.writeln(`Code: 'keyhelp'`) - ew.writeln(`Since: 'v4.26.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "KEYHELP_API_KEY": API key`) - ew.writeln(` - "KEYHELP_BASE_URL": Server URL`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "KEYHELP_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "KEYHELP_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "KEYHELP_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "KEYHELP_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/keyhelp`) - - case "leaseweb": - // generated from: providers/dns/leaseweb/leaseweb.toml - ew.writeln(`Configuration for Leaseweb.`) - ew.writeln(`Code: 'leaseweb'`) - ew.writeln(`Since: 'v4.32.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "LEASEWEB_API_KEY": API key`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "LEASEWEB_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "LEASEWEB_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "LEASEWEB_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "LEASEWEB_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/leaseweb`) - case "liara": // generated from: providers/dns/liara/liara.toml ew.writeln(`Configuration for Liara.`) @@ -2456,11 +1610,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "LIARA_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "LIARA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "LIARA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "LIARA_TEAM_ID": The team ID to access services in a team`) - ew.writeln(` - "LIARA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`) + ew.writeln(` - "LIARA_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "LIARA_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "LIARA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "LIARA_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/liara`) @@ -2480,33 +1633,12 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "AWS_SHARED_CREDENTIALS_FILE": Managed by the AWS client. Shared credentials file.`) - ew.writeln(` - "LIGHTSAIL_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "LIGHTSAIL_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) + ew.writeln(` - "LIGHTSAIL_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "LIGHTSAIL_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/lightsail`) - case "limacity": - // generated from: providers/dns/limacity/limacity.toml - ew.writeln(`Configuration for Lima-City.`) - ew.writeln(`Code: 'limacity'`) - ew.writeln(`Since: 'v4.18.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "LIMACITY_API_KEY": The API key`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "LIMACITY_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "LIMACITY_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 80)`) - ew.writeln(` - "LIMACITY_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 480)`) - ew.writeln(` - "LIMACITY_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 90)`) - ew.writeln(` - "LIMACITY_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/limacity`) - case "linode": // generated from: providers/dns/linode/linode.toml ew.writeln(`Configuration for Linode (v4).`) @@ -2519,10 +1651,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "LINODE_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "LINODE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 15)`) - ew.writeln(` - "LINODE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "LINODE_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "LINODE_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "LINODE_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "LINODE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "LINODE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/linode`) @@ -2540,10 +1672,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "LWAPI_HTTP_TIMEOUT": API request timeout in seconds (Default: 60)`) - ew.writeln(` - "LWAPI_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "LWAPI_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "LWAPI_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "LWAPI_HTTP_TIMEOUT": Maximum waiting time for the DNS records to be created (not verified)`) + ew.writeln(` - "LWAPI_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "LWAPI_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "LWAPI_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln(` - "LWAPI_URL": Liquid Web API endpoint`) ew.writeln(` - "LWAPI_ZONE": DNS Zone`) @@ -2564,10 +1696,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "LOOPIA_API_URL": API endpoint. Ex: https://api.loopia.se/RPCSERV or https://api.loopia.rs/RPCSERV`) - ew.writeln(` - "LOOPIA_HTTP_TIMEOUT": API request timeout in seconds (Default: 60)`) - ew.writeln(` - "LOOPIA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2400)`) - ew.writeln(` - "LOOPIA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "LOOPIA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "LOOPIA_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "LOOPIA_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "LOOPIA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "LOOPIA_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/loopia`) @@ -2585,10 +1717,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "LUADNS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "LUADNS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "LUADNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "LUADNS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "LUADNS_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "LUADNS_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "LUADNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "LUADNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/luadns`) @@ -2607,43 +1739,12 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "MAILINABOX_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "MAILINABOX_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 4)`) - ew.writeln(` - "MAILINABOX_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) + ew.writeln(` - "MAILINABOX_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "MAILINABOX_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/mailinabox`) - case "manageengine": - // generated from: providers/dns/manageengine/manageengine.toml - ew.writeln(`Configuration for ManageEngine CloudDNS.`) - ew.writeln(`Code: 'manageengine'`) - ew.writeln(`Since: 'v4.21.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "MANAGEENGINE_CLIENT_ID": Client ID`) - ew.writeln(` - "MANAGEENGINE_CLIENT_SECRET": Client Secret`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "MANAGEENGINE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "MANAGEENGINE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "MANAGEENGINE_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/manageengine`) - - case "manual": - // generated from: providers/dns/manual/manual.toml - ew.writeln(`Configuration for Manual.`) - ew.writeln(`Code: 'manual'`) - ew.writeln(`Since: 'v0.3.0'`) - ew.writeln() - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/manual`) - case "metaname": // generated from: providers/dns/metaname/metaname.toml ew.writeln(`Configuration for Metaname.`) @@ -2657,96 +1758,13 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "METANAME_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "METANAME_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "METANAME_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "METANAME_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "METANAME_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "METANAME_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/metaname`) - case "metaregistrar": - // generated from: providers/dns/metaregistrar/metaregistrar.toml - ew.writeln(`Configuration for Metaregistrar.`) - ew.writeln(`Code: 'metaregistrar'`) - ew.writeln(`Since: 'v4.23.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "METAREGISTRAR_API_TOKEN": The API token`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "METAREGISTRAR_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "METAREGISTRAR_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "METAREGISTRAR_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "METAREGISTRAR_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/metaregistrar`) - - case "mijnhost": - // generated from: providers/dns/mijnhost/mijnhost.toml - ew.writeln(`Configuration for mijn.host.`) - ew.writeln(`Code: 'mijnhost'`) - ew.writeln(`Since: 'v4.18.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "MIJNHOST_API_KEY": The API key`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "MIJNHOST_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "MIJNHOST_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "MIJNHOST_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "MIJNHOST_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 60)`) - ew.writeln(` - "MIJNHOST_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/mijnhost`) - - case "mittwald": - // generated from: providers/dns/mittwald/mittwald.toml - ew.writeln(`Configuration for Mittwald.`) - ew.writeln(`Code: 'mittwald'`) - ew.writeln(`Since: 'v1.48.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "MITTWALD_TOKEN": API token`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "MITTWALD_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "MITTWALD_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) - ew.writeln(` - "MITTWALD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "MITTWALD_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 120)`) - ew.writeln(` - "MITTWALD_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/mittwald`) - - case "myaddr": - // generated from: providers/dns/myaddr/myaddr.toml - ew.writeln(`Configuration for myaddr.{tools,dev,io}.`) - ew.writeln(`Code: 'myaddr'`) - ew.writeln(`Since: 'v4.22.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "MYADDR_PRIVATE_KEYS_MAPPING": Mapping between subdomains and private keys. The format is: ':,:,:'`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "MYADDR_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "MYADDR_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "MYADDR_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "MYADDR_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 2)`) - ew.writeln(` - "MYADDR_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/myaddr`) - case "mydnsjp": // generated from: providers/dns/mydnsjp/mydnsjp.toml ew.writeln(`Configuration for MyDNS.jp.`) @@ -2760,9 +1778,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "MYDNSJP_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "MYDNSJP_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "MYDNSJP_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) + ew.writeln(` - "MYDNSJP_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "MYDNSJP_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "MYDNSJP_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "MYDNSJP_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/mydnsjp`) @@ -2782,10 +1801,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "MYTHICBEASTS_API_ENDPOINT": The endpoint for the API (must implement v2)`) ew.writeln(` - "MYTHICBEASTS_AUTH_API_ENDPOINT": The endpoint for Mythic Beasts' Authentication`) - ew.writeln(` - "MYTHICBEASTS_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) - ew.writeln(` - "MYTHICBEASTS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "MYTHICBEASTS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "MYTHICBEASTS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "MYTHICBEASTS_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "MYTHICBEASTS_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "MYTHICBEASTS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "MYTHICBEASTS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/mythicbeasts`) @@ -2803,11 +1822,11 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "NAMECHEAP_HTTP_TIMEOUT": API request timeout in seconds (Default: 60)`) - ew.writeln(` - "NAMECHEAP_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 15)`) - ew.writeln(` - "NAMECHEAP_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 3600)`) + ew.writeln(` - "NAMECHEAP_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "NAMECHEAP_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "NAMECHEAP_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "NAMECHEAP_SANDBOX": Activate the sandbox (boolean)`) - ew.writeln(` - "NAMECHEAP_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "NAMECHEAP_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/namecheap`) @@ -2825,10 +1844,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "NAMECOM_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) - ew.writeln(` - "NAMECOM_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 20)`) - ew.writeln(` - "NAMECOM_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 900)`) - ew.writeln(` - "NAMECOM_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "NAMECOM_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "NAMECOM_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "NAMECOM_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "NAMECOM_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/namedotcom`) @@ -2845,37 +1864,13 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "NAMESILO_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "NAMESILO_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60), it is better to set larger than 15 minutes`) - ew.writeln(` - "NAMESILO_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600), should be in [3600, 2592000]`) + ew.writeln(` - "NAMESILO_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "NAMESILO_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation, it is better to set larger than 15m`) + ew.writeln(` - "NAMESILO_TTL": The TTL of the TXT record used for the DNS challenge, should be in [3600, 2592000]`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/namesilo`) - case "namesurfer": - // generated from: providers/dns/namesurfer/namesurfer.toml - ew.writeln(`Configuration for FusionLayer NameSurfer.`) - ew.writeln(`Code: 'namesurfer'`) - ew.writeln(`Since: 'v4.32.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "NAMESURFER_API_KEY": API key name`) - ew.writeln(` - "NAMESURFER_API_SECRET": API secret`) - ew.writeln(` - "NAMESURFER_BASE_URL": The base URL of NameSurfer API (jsonrpc10) endpoint URL (e.g., https://foo.example.com:8443/API/NSService_10)`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "NAMESURFER_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "NAMESURFER_INSECURE_SKIP_VERIFY": Whether to verify the API certificate`) - ew.writeln(` - "NAMESURFER_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "NAMESURFER_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "NAMESURFER_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) - ew.writeln(` - "NAMESURFER_VIEW": DNS view name (optional, default: empty string)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/namesurfer`) - case "nearlyfreespeech": // generated from: providers/dns/nearlyfreespeech/nearlyfreespeech.toml ew.writeln(`Configuration for NearlyFreeSpeech.NET.`) @@ -2889,35 +1884,15 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "NEARLYFREESPEECH_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "NEARLYFREESPEECH_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "NEARLYFREESPEECH_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "NEARLYFREESPEECH_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 60)`) - ew.writeln(` - "NEARLYFREESPEECH_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`) + ew.writeln(` - "NEARLYFREESPEECH_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "NEARLYFREESPEECH_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "NEARLYFREESPEECH_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "NEARLYFREESPEECH_SEQUENCE_INTERVAL": Time between sequential requests`) + ew.writeln(` - "NEARLYFREESPEECH_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/nearlyfreespeech`) - case "neodigit": - // generated from: providers/dns/neodigit/neodigit.toml - ew.writeln(`Configuration for Neodigit.`) - ew.writeln(`Code: 'neodigit'`) - ew.writeln(`Since: 'v4.30.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "NEODIGIT_TOKEN": API token`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "NEODIGIT_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "NEODIGIT_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) - ew.writeln(` - "NEODIGIT_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 300)`) - ew.writeln(` - "NEODIGIT_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/neodigit`) - case "netcup": // generated from: providers/dns/netcup/netcup.toml ew.writeln(`Configuration for Netcup.`) @@ -2932,9 +1907,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "NETCUP_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) - ew.writeln(` - "NETCUP_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 30)`) - ew.writeln(` - "NETCUP_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 900)`) + ew.writeln(` - "NETCUP_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "NETCUP_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "NETCUP_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "NETCUP_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/netcup`) @@ -2951,10 +1927,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "NETLIFY_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "NETLIFY_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "NETLIFY_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "NETLIFY_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "NETLIFY_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "NETLIFY_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "NETLIFY_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "NETLIFY_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/netlify`) @@ -2974,39 +1950,16 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "NICMANAGER_API_MODE": mode: 'anycast' or 'zones' (for FreeDNS) (default: 'anycast')`) + ew.writeln(` - "NICMANAGER_API_MODE": mode: 'anycast' or 'zone' (default: 'anycast')`) ew.writeln(` - "NICMANAGER_API_OTP": TOTP Secret (optional)`) - ew.writeln(` - "NICMANAGER_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) - ew.writeln(` - "NICMANAGER_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "NICMANAGER_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 300)`) - ew.writeln(` - "NICMANAGER_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 900)`) + ew.writeln(` - "NICMANAGER_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "NICMANAGER_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "NICMANAGER_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "NICMANAGER_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/nicmanager`) - case "nicru": - // generated from: providers/dns/nicru/nicru.toml - ew.writeln(`Configuration for RU CENTER.`) - ew.writeln(`Code: 'nicru'`) - ew.writeln(`Since: 'v4.24.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "NICRU_PASSWORD": Password for an account in RU CENTER`) - ew.writeln(` - "NICRU_SECRET": Secret for application in DNS-hosting RU CENTER`) - ew.writeln(` - "NICRU_SERVICE_ID": Service ID for application in DNS-hosting RU CENTER`) - ew.writeln(` - "NICRU_SERVICE_NAME": Service Name for DNS-hosting RU CENTER`) - ew.writeln(` - "NICRU_USER": Agreement for an account in RU CENTER`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "NICRU_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 60)`) - ew.writeln(` - "NICRU_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 600)`) - ew.writeln(` - "NICRU_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 30)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/nicru`) - case "nifcloud": // generated from: providers/dns/nifcloud/nifcloud.toml ew.writeln(`Configuration for NIFCloud.`) @@ -3020,10 +1973,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "NIFCLOUD_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "NIFCLOUD_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "NIFCLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "NIFCLOUD_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "NIFCLOUD_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "NIFCLOUD_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "NIFCLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "NIFCLOUD_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/nifcloud`) @@ -3040,10 +1993,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "NJALLA_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "NJALLA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "NJALLA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "NJALLA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "NJALLA_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "NJALLA_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "NJALLA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "NJALLA_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/njalla`) @@ -3060,10 +2013,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "NODION_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "NODION_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "NODION_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "NODION_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "NODION_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "NODION_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "NODION_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "NODION_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/nodion`) @@ -3080,34 +2033,14 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "NS1_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) - ew.writeln(` - "NS1_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "NS1_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "NS1_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "NS1_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "NS1_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "NS1_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "NS1_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/ns1`) - case "octenium": - // generated from: providers/dns/octenium/octenium.toml - ew.writeln(`Configuration for Octenium.`) - ew.writeln(`Code: 'octenium'`) - ew.writeln(`Since: 'v4.27.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "OCTENIUM_API_KEY": API key`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "OCTENIUM_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "OCTENIUM_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "OCTENIUM_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "OCTENIUM_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/octenium`) - case "oraclecloud": // generated from: providers/dns/oraclecloud/oraclecloud.toml ew.writeln(`Configuration for Oracle Cloud.`) @@ -3117,25 +2050,18 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Credentials:`) ew.writeln(` - "OCI_COMPARTMENT_OCID": Compartment OCID`) - ew.writeln(` - "OCI_FINGERPRINT": Public key fingerprint (ignored if 'OCI_AUTH_TYPE=instance_principal')`) - ew.writeln(` - "OCI_PRIVATE_KEY_PASSWORD": Private key password (ignored if 'OCI_AUTH_TYPE=instance_principal')`) - ew.writeln(` - "OCI_PRIVATE_KEY_PATH": Private key file (ignored if 'OCI_AUTH_TYPE=instance_principal')`) - ew.writeln(` - "OCI_REGION": Region (it can be empty if 'OCI_AUTH_TYPE=instance_principal').`) - ew.writeln(` - "OCI_TENANCY_OCID": Tenancy OCID (ignored if 'OCI_AUTH_TYPE=instance_principal')`) - ew.writeln(` - "OCI_USER_OCID": User OCID (ignored if 'OCI_AUTH_TYPE=instance_principal')`) + ew.writeln(` - "OCI_PRIVKEY_FILE": Private key file`) + ew.writeln(` - "OCI_PRIVKEY_PASS": Private key password`) + ew.writeln(` - "OCI_PUBKEY_FINGERPRINT": Public key fingerprint`) + ew.writeln(` - "OCI_REGION": Region`) + ew.writeln(` - "OCI_TENANCY_OCID": Tenancy OCID`) + ew.writeln(` - "OCI_USER_OCID": User OCID`) ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "OCI_AUTH_TYPE": Authorization type. Possible values: 'instance_principal', '' (Default: '')`) - ew.writeln(` - "OCI_HTTP_TIMEOUT": API request timeout in seconds (Default: 60)`) - ew.writeln(` - "OCI_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "OCI_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "OCI_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - ew.writeln(` - "TF_VAR_fingerprint": Alias on 'OCI_FINGERPRINT'`) - ew.writeln(` - "TF_VAR_private_key_path": Alias on 'OCI_PRIVATE_KEY_PATH'`) - ew.writeln(` - "TF_VAR_region": Alias on 'OCI_REGION'`) - ew.writeln(` - "TF_VAR_tenancy_ocid": Alias on 'OCI_TENANCY_OCID'`) - ew.writeln(` - "TF_VAR_user_ocid": Alias on 'OCI_USER_OCID'`) + ew.writeln(` - "OCI_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "OCI_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "OCI_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/oraclecloud`) @@ -3149,19 +2075,18 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Credentials:`) ew.writeln(` - "OTC_DOMAIN_NAME": Domain name`) + ew.writeln(` - "OTC_IDENTITY_ENDPOINT": Identity endpoint URL`) ew.writeln(` - "OTC_PASSWORD": Password`) ew.writeln(` - "OTC_PROJECT_NAME": Project name`) ew.writeln(` - "OTC_USER_NAME": User name`) ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "OTC_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) - ew.writeln(` - "OTC_IDENTITY_ENDPOINT": Identity endpoint URL (default: https://iam.eu-de.otc.t-systems.com:443/v3/auth/tokens)`) - ew.writeln(` - "OTC_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "OTC_PRIVATE_ZONE": Set to true to use private zones only (default: use public zones only)`) - ew.writeln(` - "OTC_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "OTC_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 60)`) - ew.writeln(` - "OTC_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "OTC_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "OTC_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "OTC_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "OTC_SEQUENCE_INTERVAL": Time between sequential requests`) + ew.writeln(` - "OTC_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/otc`) @@ -3174,7 +2099,6 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Credentials:`) - ew.writeln(` - "OVH_ACCESS_TOKEN": Access token`) ew.writeln(` - "OVH_APPLICATION_KEY": Application key (Application Key authentication)`) ew.writeln(` - "OVH_APPLICATION_SECRET": Application secret (Application Key authentication)`) ew.writeln(` - "OVH_CLIENT_ID": Client ID (OAuth2)`) @@ -3184,10 +2108,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "OVH_HTTP_TIMEOUT": API request timeout in seconds (Default: 180)`) - ew.writeln(` - "OVH_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "OVH_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "OVH_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "OVH_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "OVH_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "OVH_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "OVH_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/ovh`) @@ -3206,11 +2130,11 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "PDNS_API_VERSION": Skip API version autodetection and use the provided version number.`) - ew.writeln(` - "PDNS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "PDNS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "PDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) + ew.writeln(` - "PDNS_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "PDNS_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "PDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "PDNS_SERVER_NAME": Name of the server in the URL, 'localhost' by default`) - ew.writeln(` - "PDNS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "PDNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/pdns`) @@ -3229,10 +2153,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "PLESK_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "PLESK_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "PLESK_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "PLESK_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "PLESK_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "PLESK_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "PLESK_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "PLESK_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/plesk`) @@ -3250,10 +2174,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "PORKBUN_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "PORKBUN_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) - ew.writeln(` - "PORKBUN_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 600)`) - ew.writeln(` - "PORKBUN_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "PORKBUN_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "PORKBUN_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "PORKBUN_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "PORKBUN_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/porkbun`) @@ -3271,34 +2195,14 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "RACKSPACE_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "RACKSPACE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 3)`) - ew.writeln(` - "RACKSPACE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "RACKSPACE_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "RACKSPACE_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "RACKSPACE_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "RACKSPACE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "RACKSPACE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/rackspace`) - case "rainyun": - // generated from: providers/dns/rainyun/rainyun.toml - ew.writeln(`Configuration for Rain Yun/雨云.`) - ew.writeln(`Code: 'rainyun'`) - ew.writeln(`Since: 'v4.21.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "RAINYUN_API_KEY": API key`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "RAINYUN_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "RAINYUN_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "RAINYUN_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "RAINYUN_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/rainyun`) - case "rcodezero": // generated from: providers/dns/rcodezero/rcodezero.toml ew.writeln(`Configuration for RcodeZero.`) @@ -3311,34 +2215,14 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "RCODEZERO_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "RCODEZERO_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) - ew.writeln(` - "RCODEZERO_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 240)`) - ew.writeln(` - "RCODEZERO_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "RCODEZERO_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "RCODEZERO_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "RCODEZERO_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "RCODEZERO_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/rcodezero`) - case "regfish": - // generated from: providers/dns/regfish/regfish.toml - ew.writeln(`Configuration for Regfish.`) - ew.writeln(`Code: 'regfish'`) - ew.writeln(`Since: 'v4.20.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "REGFISH_API_KEY": API key`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "REGFISH_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "REGFISH_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "REGFISH_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "REGFISH_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/regfish`) - case "regru": // generated from: providers/dns/regru/regru.toml ew.writeln(`Configuration for reg.ru.`) @@ -3352,12 +2236,12 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "REGRU_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "REGRU_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "REGRU_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "REGRU_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "REGRU_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "REGRU_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "REGRU_TLS_CERT": authentication certificate`) ew.writeln(` - "REGRU_TLS_KEY": authentication private key`) - ew.writeln(` - "REGRU_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "REGRU_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/regru`) @@ -3371,18 +2255,17 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Credentials:`) ew.writeln(` - "RFC2136_NAMESERVER": Network address in the form "host" or "host:port"`) - ew.writeln(` - "RFC2136_TSIG_ALGORITHM": TSIG algorithm. See [miekg/dns#tsig.go](https://github.com/miekg/dns/blob/master/tsig.go) for supported values. To disable TSIG authentication, leave the 'RFC2136_TSIG_KEY' or 'RFC2136_TSIG_SECRET' variables unset.`) - ew.writeln(` - "RFC2136_TSIG_KEY": Name of the secret key as defined in DNS server configuration. To disable TSIG authentication, leave the 'RFC2136_TSIG_KEY' variable unset.`) - ew.writeln(` - "RFC2136_TSIG_SECRET": Secret key payload. To disable TSIG authentication, leave the 'RFC2136_TSIG_SECRET' variable unset.`) + ew.writeln(` - "RFC2136_TSIG_ALGORITHM": TSIG algorithm. See [miekg/dns#tsig.go](https://github.com/miekg/dns/blob/master/tsig.go) for supported values. To disable TSIG authentication, leave the 'RFC2136_TSIG*' variables unset.`) + ew.writeln(` - "RFC2136_TSIG_KEY": Name of the secret key as defined in DNS server configuration. To disable TSIG authentication, leave the 'RFC2136_TSIG*' variables unset.`) + ew.writeln(` - "RFC2136_TSIG_SECRET": Secret key payload. To disable TSIG authentication, leave the' RFC2136_TSIG*' variables unset.`) ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "RFC2136_DNS_TIMEOUT": API request timeout in seconds (Default: 10)`) - ew.writeln(` - "RFC2136_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "RFC2136_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "RFC2136_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 60)`) - ew.writeln(` - "RFC2136_TSIG_FILE": Path to a key file generated by tsig-keygen`) - ew.writeln(` - "RFC2136_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "RFC2136_DNS_TIMEOUT": API request timeout`) + ew.writeln(` - "RFC2136_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "RFC2136_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "RFC2136_SEQUENCE_INTERVAL": Time between sequential requests`) + ew.writeln(` - "RFC2136_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/rfc2136`) @@ -3399,10 +2282,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "RIMUHOSTING_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "RIMUHOSTING_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "RIMUHOSTING_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "RIMUHOSTING_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`) + ew.writeln(` - "RIMUHOSTING_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "RIMUHOSTING_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "RIMUHOSTING_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "RIMUHOSTING_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/rimuhosting`) @@ -3428,18 +2311,17 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "AWS_MAX_RETRIES": The number of maximum returns the service will use to make an individual API request`) - ew.writeln(` - "AWS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 4)`) - ew.writeln(` - "AWS_PRIVATE_ZONE": Set to true to use private zones only (default: use public zones only)`) - ew.writeln(` - "AWS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) + ew.writeln(` - "AWS_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "AWS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "AWS_SHARED_CREDENTIALS_FILE": Managed by the AWS client. Shared credentials file.`) - ew.writeln(` - "AWS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 10)`) + ew.writeln(` - "AWS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/route53`) case "safedns": // generated from: providers/dns/safedns/safedns.toml - ew.writeln(`Configuration for ANS SafeDNS.`) + ew.writeln(`Configuration for UKFast SafeDNS.`) ew.writeln(`Code: 'safedns'`) ew.writeln(`Since: 'v4.6.0'`) ew.writeln() @@ -3449,10 +2331,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "SAFEDNS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "SAFEDNS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "SAFEDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "SAFEDNS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "SAFEDNS_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "SAFEDNS_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "SAFEDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "SAFEDNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/safedns`) @@ -3470,10 +2352,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "SAKURACLOUD_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) - ew.writeln(` - "SAKURACLOUD_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "SAKURACLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "SAKURACLOUD_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "SAKURACLOUD_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "SAKURACLOUD_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "SAKURACLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "SAKURACLOUD_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/sakuracloud`) @@ -3492,10 +2374,9 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "SCW_ACCESS_KEY": Access key`) - ew.writeln(` - "SCW_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "SCW_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) - ew.writeln(` - "SCW_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "SCW_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) + ew.writeln(` - "SCW_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "SCW_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "SCW_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/scaleway`) @@ -3513,10 +2394,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "SELECTEL_BASE_URL": API endpoint URL`) - ew.writeln(` - "SELECTEL_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "SELECTEL_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "SELECTEL_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "SELECTEL_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) + ew.writeln(` - "SELECTEL_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "SELECTEL_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "SELECTEL_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "SELECTEL_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/selectel`) @@ -3536,40 +2417,15 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "SELECTELV2_AUTH_REGION": Location for auth endpoint like ResellAPI or Keystone (default: 'ru-1')`) - ew.writeln(` - "SELECTELV2_AUTH_URL": Identity endpoint (defaul: 'https://cloud.api.selcloud.ru/identity/v3/')`) ew.writeln(` - "SELECTELV2_BASE_URL": API endpoint URL`) - ew.writeln(` - "SELECTELV2_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "SELECTELV2_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 5)`) - ew.writeln(` - "SELECTELV2_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "SELECTELV2_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) - ew.writeln(` - "SELECTELV2_USER_DOMAIN_NAME": To specify the domain name (account ID) where the user is located. (default: SELECTELV2_ACCOUNT_ID)`) + ew.writeln(` - "SELECTELV2_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "SELECTELV2_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "SELECTELV2_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "SELECTELV2_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/selectelv2`) - case "selfhostde": - // generated from: providers/dns/selfhostde/selfhostde.toml - ew.writeln(`Configuration for SelfHost.(de|eu).`) - ew.writeln(`Code: 'selfhostde'`) - ew.writeln(`Since: 'v4.19.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "SELFHOSTDE_PASSWORD": Password`) - ew.writeln(` - "SELFHOSTDE_RECORDS_MAPPING": Record IDs mapping with domains (ex: example.com:123:456,example.org:789,foo.example.com:147)`) - ew.writeln(` - "SELFHOSTDE_USERNAME": Username`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "SELFHOSTDE_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "SELFHOSTDE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 30)`) - ew.writeln(` - "SELFHOSTDE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 240)`) - ew.writeln(` - "SELFHOSTDE_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/selfhostde`) - case "servercow": // generated from: providers/dns/servercow/servercow.toml ew.writeln(`Configuration for Servercow.`) @@ -3583,10 +2439,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "SERVERCOW_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "SERVERCOW_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "SERVERCOW_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "SERVERCOW_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "SERVERCOW_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "SERVERCOW_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "SERVERCOW_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "SERVERCOW_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/servercow`) @@ -3604,10 +2460,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "SHELLRENT_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "SHELLRENT_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) - ew.writeln(` - "SHELLRENT_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 300)`) - ew.writeln(` - "SHELLRENT_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`) + ew.writeln(` - "SHELLRENT_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "SHELLRENT_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "SHELLRENT_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "SHELLRENT_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/shellrent`) @@ -3625,10 +2481,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "SIMPLY_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "SIMPLY_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) - ew.writeln(` - "SIMPLY_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 300)`) - ew.writeln(` - "SIMPLY_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "SIMPLY_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "SIMPLY_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "SIMPLY_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "SIMPLY_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/simply`) @@ -3646,36 +2502,15 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "SONIC_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) - ew.writeln(` - "SONIC_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "SONIC_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "SONIC_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 60)`) - ew.writeln(` - "SONIC_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "SONIC_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "SONIC_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "SONIC_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "SONIC_SEQUENCE_INTERVAL": Time between sequential requests`) + ew.writeln(` - "SONIC_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/sonic`) - case "spaceship": - // generated from: providers/dns/spaceship/spaceship.toml - ew.writeln(`Configuration for Spaceship.`) - ew.writeln(`Code: 'spaceship'`) - ew.writeln(`Since: 'v4.22.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "SPACESHIP_API_KEY": API key`) - ew.writeln(` - "SPACESHIP_API_SECRET": API secret`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "SPACESHIP_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "SPACESHIP_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "SPACESHIP_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "SPACESHIP_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/spaceship`) - case "stackpath": // generated from: providers/dns/stackpath/stackpath.toml ew.writeln(`Configuration for Stackpath.`) @@ -3690,54 +2525,13 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "STACKPATH_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "STACKPATH_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "STACKPATH_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "STACKPATH_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "STACKPATH_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "STACKPATH_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/stackpath`) - case "syse": - // generated from: providers/dns/syse/syse.toml - ew.writeln(`Configuration for Syse.`) - ew.writeln(`Code: 'syse'`) - ew.writeln(`Since: 'v4.30.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "SYSE_CREDENTIALS": Comma-separated list of 'zone:password' credential pairs`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "SYSE_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "SYSE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) - ew.writeln(` - "SYSE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 1200)`) - ew.writeln(` - "SYSE_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/syse`) - - case "technitium": - // generated from: providers/dns/technitium/technitium.toml - ew.writeln(`Configuration for Technitium.`) - ew.writeln(`Code: 'technitium'`) - ew.writeln(`Since: 'v4.20.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "TECHNITIUM_API_TOKEN": API token`) - ew.writeln(` - "TECHNITIUM_SERVER_BASE_URL": Server base URL`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "TECHNITIUM_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "TECHNITIUM_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "TECHNITIUM_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "TECHNITIUM_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/technitium`) - case "tencentcloud": // generated from: providers/dns/tencentcloud/tencentcloud.toml ew.writeln(`Configuration for Tencent Cloud DNS.`) @@ -3751,56 +2545,16 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "TENCENTCLOUD_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "TENCENTCLOUD_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "TENCENTCLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "TENCENTCLOUD_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "TENCENTCLOUD_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "TENCENTCLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "TENCENTCLOUD_REGION": Region`) ew.writeln(` - "TENCENTCLOUD_SESSION_TOKEN": Access Key token`) - ew.writeln(` - "TENCENTCLOUD_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`) + ew.writeln(` - "TENCENTCLOUD_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/tencentcloud`) - case "timewebcloud": - // generated from: providers/dns/timewebcloud/timewebcloud.toml - ew.writeln(`Configuration for Timeweb Cloud.`) - ew.writeln(`Code: 'timewebcloud'`) - ew.writeln(`Since: 'v4.20.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "TIMEWEBCLOUD_AUTH_TOKEN": Authentication token`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "TIMEWEBCLOUD_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) - ew.writeln(` - "TIMEWEBCLOUD_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "TIMEWEBCLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/timewebcloud`) - - case "todaynic": - // generated from: providers/dns/todaynic/todaynic.toml - ew.writeln(`Configuration for TodayNIC/时代互联.`) - ew.writeln(`Code: 'todaynic'`) - ew.writeln(`Since: 'v4.32.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "TODAYNIC_API_KEY": API key`) - ew.writeln(` - "TODAYNIC_AUTH_USER_ID": account ID`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "TODAYNIC_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "TODAYNIC_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "TODAYNIC_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "TODAYNIC_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/todaynic`) - case "transip": // generated from: providers/dns/transip/transip.toml ew.writeln(`Configuration for TransIP.`) @@ -3814,10 +2568,9 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "TRANSIP_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "TRANSIP_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) - ew.writeln(` - "TRANSIP_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 600)`) - ew.writeln(` - "TRANSIP_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 10)`) + ew.writeln(` - "TRANSIP_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "TRANSIP_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "TRANSIP_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/transip`) @@ -3836,33 +2589,13 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "ULTRADNS_ENDPOINT": API endpoint URL, defaults to https://api.ultradns.com/`) - ew.writeln(` - "ULTRADNS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 4)`) - ew.writeln(` - "ULTRADNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "ULTRADNS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "ULTRADNS_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "ULTRADNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "ULTRADNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/ultradns`) - case "uniteddomains": - // generated from: providers/dns/uniteddomains/uniteddomains.toml - ew.writeln(`Configuration for United-Domains.`) - ew.writeln(`Code: 'uniteddomains'`) - ew.writeln(`Since: 'v4.29.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "UNITEDDOMAINS_API_KEY": API key '.' https://www.united-domains.de/help/faq-article/getting-started-with-the-united-domains-dns-api/`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "UNITEDDOMAINS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "UNITEDDOMAINS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "UNITEDDOMAINS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 900)`) - ew.writeln(` - "UNITEDDOMAINS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/uniteddomains`) - case "variomedia": // generated from: providers/dns/variomedia/variomedia.toml ew.writeln(`Configuration for Variomedia.`) @@ -3875,11 +2608,11 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "VARIOMEDIA_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "VARIOMEDIA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "VARIOMEDIA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "VARIOMEDIA_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 60)`) - ew.writeln(` - "VARIOMEDIA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "DODE_SEQUENCE_INTERVAL": Time between sequential requests`) + ew.writeln(` - "VARIOMEDIA_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "VARIOMEDIA_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "VARIOMEDIA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "VARIOMEDIA_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/variomedia`) @@ -3898,9 +2631,9 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "VEGADNS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 60)`) - ew.writeln(` - "VEGADNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 720)`) - ew.writeln(` - "VEGADNS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 10)`) + ew.writeln(` - "VEGADNS_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "VEGADNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "VEGADNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/vegadns`) @@ -3917,11 +2650,11 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "VERCEL_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "VERCEL_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 5)`) - ew.writeln(` - "VERCEL_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "VERCEL_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "VERCEL_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "VERCEL_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "VERCEL_TEAM_ID": Team ID (ex: team_xxxxxxxxxxxxxxxxxxxxxxxx)`) - ew.writeln(` - "VERCEL_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) + ew.writeln(` - "VERCEL_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/vercel`) @@ -3940,11 +2673,11 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "VERSIO_ENDPOINT": The endpoint URL of the API Server`) - ew.writeln(` - "VERSIO_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "VERSIO_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 5)`) - ew.writeln(` - "VERSIO_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "VERSIO_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 60)`) - ew.writeln(` - "VERSIO_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "VERSIO_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "VERSIO_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "VERSIO_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "VERSIO_SEQUENCE_INTERVAL": Time between sequential requests, default 60s`) + ew.writeln(` - "VERSIO_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/versio`) @@ -3963,35 +2696,13 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "VINYLDNS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "VINYLDNS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 4)`) - ew.writeln(` - "VINYLDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "VINYLDNS_QUOTE_VALUE": Adds quotes around the TXT record value (Default: false)`) - ew.writeln(` - "VINYLDNS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 30)`) + ew.writeln(` - "VINYLDNS_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "VINYLDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "VINYLDNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/vinyldns`) - case "virtualname": - // generated from: providers/dns/virtualname/virtualname.toml - ew.writeln(`Configuration for Virtualname.`) - ew.writeln(`Code: 'virtualname'`) - ew.writeln(`Since: 'v4.30.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "VIRTUALNAME_TOKEN": API token`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "VIRTUALNAME_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "VIRTUALNAME_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) - ew.writeln(` - "VIRTUALNAME_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 300)`) - ew.writeln(` - "VIRTUALNAME_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/virtualname`) - case "vkcloud": // generated from: providers/dns/vkcloud/vkcloud.toml ew.writeln(`Configuration for VK Cloud.`) @@ -4009,37 +2720,13 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(` - "VK_CLOUD_DNS_ENDPOINT": URL of DNS API. Defaults to https://mcs.mail.ru/public-dns but can be changed for usage with private clouds`) ew.writeln(` - "VK_CLOUD_DOMAIN_NAME": Openstack users domain name. Defaults to 'users' but can be changed for usage with private clouds`) ew.writeln(` - "VK_CLOUD_IDENTITY_ENDPOINT": URL of OpenStack Auth API, Defaults to https://infra.mail.ru:35357/v3/ but can be changed for usage with private clouds`) - ew.writeln(` - "VK_CLOUD_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "VK_CLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "VK_CLOUD_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) + ew.writeln(` - "VK_CLOUD_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "VK_CLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "VK_CLOUD_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/vkcloud`) - case "volcengine": - // generated from: providers/dns/volcengine/volcengine.toml - ew.writeln(`Configuration for Volcano Engine/火山引擎.`) - ew.writeln(`Code: 'volcengine'`) - ew.writeln(`Since: 'v4.19.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "VOLC_ACCESSKEY": Access Key ID (AK)`) - ew.writeln(` - "VOLC_SECRETKEY": Secret Access Key (SK)`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "VOLC_HOST": API host`) - ew.writeln(` - "VOLC_HTTP_TIMEOUT": API request timeout in seconds (Default: 15)`) - ew.writeln(` - "VOLC_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) - ew.writeln(` - "VOLC_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 240)`) - ew.writeln(` - "VOLC_REGION": Region`) - ew.writeln(` - "VOLC_SCHEME": API scheme`) - ew.writeln(` - "VOLC_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/volcengine`) - case "vscale": // generated from: providers/dns/vscale/vscale.toml ew.writeln(`Configuration for Vscale.`) @@ -4053,10 +2740,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "VSCALE_BASE_URL": API endpoint URL`) - ew.writeln(` - "VSCALE_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "VSCALE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "VSCALE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "VSCALE_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) + ew.writeln(` - "VSCALE_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "VSCALE_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "VSCALE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "VSCALE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/vscale`) @@ -4073,54 +2760,34 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "VULTR_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "VULTR_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "VULTR_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "VULTR_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + ew.writeln(` - "VULTR_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "VULTR_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "VULTR_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "VULTR_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/vultr`) case "webnames": // generated from: providers/dns/webnames/webnames.toml - ew.writeln(`Configuration for webnames.ru.`) + ew.writeln(`Configuration for Webnames.`) ew.writeln(`Code: 'webnames'`) ew.writeln(`Since: 'v4.15.0'`) ew.writeln() ew.writeln(`Credentials:`) - ew.writeln(` - "WEBNAMESRU_API_KEY": Domain API key`) + ew.writeln(` - "WEBNAMES_API_KEY": Domain API key`) ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "WEBNAMESRU_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "WEBNAMESRU_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "WEBNAMESRU_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "WEBNAMES_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "WEBNAMES_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "WEBNAMES_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "WEBNAMES_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/webnames`) - case "webnamesca": - // generated from: providers/dns/webnamesca/webnamesca.toml - ew.writeln(`Configuration for webnames.ca.`) - ew.writeln(`Code: 'webnamesca'`) - ew.writeln(`Since: 'v4.28.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "WEBNAMESCA_API_KEY": API key`) - ew.writeln(` - "WEBNAMESCA_API_USER": API username`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "WEBNAMESCA_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "WEBNAMESCA_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "WEBNAMESCA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "WEBNAMESCA_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/webnamesca`) - case "websupport": // generated from: providers/dns/websupport/websupport.toml ew.writeln(`Configuration for Websupport.`) @@ -4134,11 +2801,11 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "WEBSUPPORT_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "WEBSUPPORT_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "WEBSUPPORT_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "WEBSUPPORT_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 60)`) - ew.writeln(` - "WEBSUPPORT_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`) + ew.writeln(` - "WEBSUPPORT_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "WEBSUPPORT_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "WEBSUPPORT_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "WEBSUPPORT_SEQUENCE_INTERVAL": Time between sequential requests`) + ew.writeln(` - "WEBSUPPORT_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/websupport`) @@ -4156,35 +2823,14 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "WEDOS_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "WEDOS_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) - ew.writeln(` - "WEDOS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 600)`) - ew.writeln(` - "WEDOS_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) + ew.writeln(` - "WEDOS_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "WEDOS_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "WEDOS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "WEDOS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/wedos`) - case "westcn": - // generated from: providers/dns/westcn/westcn.toml - ew.writeln(`Configuration for West.cn/西部数码.`) - ew.writeln(`Code: 'westcn'`) - ew.writeln(`Since: 'v4.21.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "WESTCN_PASSWORD": API password`) - ew.writeln(` - "WESTCN_USERNAME": Username`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "WESTCN_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "WESTCN_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 10)`) - ew.writeln(` - "WESTCN_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 120)`) - ew.writeln(` - "WESTCN_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/westcn`) - case "yandex": // generated from: providers/dns/yandex/yandex.toml ew.writeln(`Configuration for Yandex PDD.`) @@ -4197,10 +2843,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "YANDEX_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "YANDEX_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "YANDEX_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "YANDEX_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 21600)`) + ew.writeln(` - "YANDEX_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "YANDEX_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "YANDEX_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "YANDEX_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/yandex`) @@ -4218,10 +2864,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "YANDEX360_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "YANDEX360_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "YANDEX360_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "YANDEX360_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 21600)`) + ew.writeln(` - "YANDEX360_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "YANDEX360_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "YANDEX360_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "YANDEX360_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/yandex360`) @@ -4239,33 +2885,13 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "YANDEX_CLOUD_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "YANDEX_CLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "YANDEX_CLOUD_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`) + ew.writeln(` - "YANDEX_CLOUD_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "YANDEX_CLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "YANDEX_CLOUD_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/yandexcloud`) - case "zoneedit": - // generated from: providers/dns/zoneedit/zoneedit.toml - ew.writeln(`Configuration for ZoneEdit.`) - ew.writeln(`Code: 'zoneedit'`) - ew.writeln(`Since: 'v4.25.0'`) - ew.writeln() - - ew.writeln(`Credentials:`) - ew.writeln(` - "ZONEEDIT_AUTH_TOKEN": Authentication token`) - ew.writeln(` - "ZONEEDIT_USER": User ID`) - ew.writeln() - - ew.writeln(`Additional Configuration:`) - ew.writeln(` - "ZONEEDIT_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "ZONEEDIT_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "ZONEEDIT_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - - ew.writeln() - ew.writeln(`More information: https://go-acme.github.io/lego/dns/zoneedit`) - case "zoneee": // generated from: providers/dns/zoneee/zoneee.toml ew.writeln(`Configuration for Zone.ee.`) @@ -4280,9 +2906,10 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "ZONEEE_ENDPOINT": API endpoint URL`) - ew.writeln(` - "ZONEEE_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "ZONEEE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 5)`) - ew.writeln(` - "ZONEEE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 300)`) + ew.writeln(` - "ZONEEE_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "ZONEEE_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "ZONEEE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "ZONEEE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/zoneee`) @@ -4299,14 +2926,16 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`Additional Configuration:`) - ew.writeln(` - "ZONOMI_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`) - ew.writeln(` - "ZONOMI_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) - ew.writeln(` - "ZONOMI_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) - ew.writeln(` - "ZONOMI_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`) + ew.writeln(` - "ZONOMI_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "ZONOMI_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "ZONOMI_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "ZONOMI_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/zonomi`) + case "manual": + ew.writeln(`Solving the DNS-01 challenge using CLI prompt.`) default: return fmt.Errorf("%q is not yet supported", name) } diff --git a/docs/Makefile b/docs/Makefile index 6c84c7d1d..8e32681d1 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,14 +1,14 @@ -.PHONY: default clean serve build +.PHONY: default clean hugo hugo-build -default: clean serve +default: clean hugo clean: rm -rf public/ -build: clean +hugo-build: clean hugo --enableGitInfo --source . -serve: +hugo: hugo server --disableFastRender --enableGitInfo --watch --source . # hugo server -D diff --git a/docs/content/_index.md b/docs/content/_index.md index 95e411afc..a0e20523f 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -1,40 +1,31 @@ --- -title: "Lego" +title: "Welcome" date: 2019-03-03T16:39:46+01:00 draft: false -chapter: false +chapter: true --- +# Lego + Let's Encrypt client and ACME library written in Go. -{{% notice important %}} -lego is an independent, free, and open-source project, if you value it, consider [supporting it](https://donate.ldez.dev)! ❤️ - -This project is not owned by a company. I'm not an employee of a company. - -I don't have gifted domains/accounts from DNS companies. - -I've been maintaining it for about 10 years. -{{% /notice %}} - ## Features - ACME v2 [RFC 8555](https://www.rfc-editor.org/rfc/rfc8555.html) - Support [RFC 8737](https://www.rfc-editor.org/rfc/rfc8737.html): TLS Application‑Layer Protocol Negotiation (ALPN) Challenge Extension - Support [RFC 8738](https://www.rfc-editor.org/rfc/rfc8738.html): issues certificates for IP addresses - - Support [RFC 9773](https://www.rfc-editor.org/rfc/rfc9773.html): Renewal Information (ARI) Extension - - Support [draft-ietf-acme-profiles-00](https://datatracker.ietf.org/doc/draft-ietf-acme-profiles/): Profiles Extension -- Comes with about [180 DNS providers]({{% ref "dns" %}}) + - Support [draft-ietf-acme-ari-01](https://datatracker.ietf.org/doc/draft-ietf-acme-ari/): Renewal Information (ARI) Extension - Register with CA - Obtain certificates, both from scratch or with an existing CSR - Renew certificates - Revoke certificates -- Robust implementation of ACME challenges: +- Robust implementation of all ACME challenges - HTTP (http-01) - DNS (dns-01) - TLS (tls-alpn-01) - SAN certificate support - [CNAME support](https://letsencrypt.org/2019/10/09/onboarding-your-customers-with-lets-encrypt-and-acme.html) by default -- [Custom challenge solvers]({{% ref "usage/library/Writing-a-Challenge-Solver" %}}) +- Comes with multiple optional [DNS providers]({{< ref "dns" >}}) +- [Custom challenge solvers]({{< ref "usage/library/Writing-a-Challenge-Solver" >}}) - Certificate bundling - OCSP helper function diff --git a/docs/content/dns/_index.md b/docs/content/dns/_index.md index 2b6f0489c..eba5e4b7f 100644 --- a/docs/content/dns/_index.md +++ b/docs/content/dns/_index.md @@ -5,16 +5,6 @@ draft: false weight: 3 --- -{{% notice important %}} -lego is an independent, free, and open-source project, if you value it, consider [supporting it](https://donate.ldez.dev)! ❤️ - -This project is not owned by a company. I'm not an employee of a company. - -I don't have gifted domains/accounts from DNS companies. - -I've been maintaining it for about 10 years. -{{% /notice %}} - ## Configuration and Credentials Credentials and DNS configuration for DNS providers must be passed through environment variables. @@ -25,7 +15,7 @@ The environment variables can reference a value. Here is an example bash command using the Cloudflare DNS provider: -```bash +```console $ CLOUDFLARE_EMAIL=you@example.com \ CLOUDFLARE_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \ lego --dns cloudflare --domains www.example.com --email you@example.com run @@ -43,7 +33,7 @@ The file must contain only the value. Here is an example bash command using the CloudFlare DNS provider: -```bash +```console $ cat /the/path/to/my/key b9841238feb177a84330febba8a83208921177bffe733 diff --git a/providers/dns/manual/manual.toml b/docs/content/dns/manual.md similarity index 76% rename from providers/dns/manual/manual.toml rename to docs/content/dns/manual.md index fc47a8fae..3f9cf0a8e 100644 --- a/providers/dns/manual/manual.toml +++ b/docs/content/dns/manual.md @@ -1,19 +1,24 @@ -Name = "Manual" -Description = '''Solving the DNS-01 challenge using CLI prompt.''' -Code = "manual" -Since = "v0.3.0" +--- +title: "Manual" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: manual +dnsprovider: + since: v0.3.0 + code: manual + url: +--- -Example = ''' -lego --dns manual -d '*.example.com' -d example.com run -''' +Solving the DNS-01 challenge using CLI prompt. + + -Additional = ''' ## Example To start using the CLI prompt "provider", start lego with `--dns manual`: ```console -$ lego --dns manual -d example.com run +$ lego --email "you@example.com" --domains="example.com" --dns "manual" run ``` What follows are a few log print-outs, interspersed with some prompts, asking for you to do perform some actions: @@ -31,13 +36,13 @@ If you accept the linked Terms of Service, hit `Enter`. [INFO] acme: Registering account for you@example.com !!!! HEADS UP !!!! -Your account credentials have been saved in your -configuration directory at "./.lego/accounts". + Your account credentials have been saved in your Let's Encrypt + configuration directory at "./.lego/accounts". -You should make a secure backup of this folder now. This -configuration directory will also contain private keys -generated by lego and certificates obtained from the ACME -server. Making regular backups of this folder is ideal. + You should make a secure backup of this folder now. This + configuration directory will also contain certificates and + private keys obtained from Let's Encrypt so making regular + backups of this folder is ideal. [INFO] [example.com] acme: Obtaining bundled SAN certificate [INFO] [example.com] AuthURL: https://acme-v02.api.letsencrypt.org/acme/authz-v3/2345678901 [INFO] [example.com] acme: Could not find solver for: tls-alpn-01 @@ -65,5 +70,3 @@ _acme-challenge.example.com. 120 IN TXT "hX0dPkG6Gfs9hUvBAchQclkyyoEKbShbpvJ9mY5 ``` As mentioned, you can now remove the TXT record again. - -''' diff --git a/docs/content/dns/zz_gen_acme-dns.md b/docs/content/dns/zz_gen_acme-dns.md index 5564dba1b..de51827e8 100644 --- a/docs/content/dns/zz_gen_acme-dns.md +++ b/docs/content/dns/zz_gen_acme-dns.md @@ -28,13 +28,7 @@ Here is an example bash command using the Joohoi's ACME-DNS provider: ```bash ACME_DNS_API_BASE=http://10.0.0.8:4443 \ ACME_DNS_STORAGE_PATH=/root/.lego-acme-dns-accounts.json \ -lego --dns "acme-dns" -d '*.example.com' -d example.com run - -# or - -ACME_DNS_API_BASE=http://10.0.0.8:4443 \ -ACME_DNS_STORAGE_BASE_URL=http://10.10.10.10:80 \ -lego --dns "acme-dns" -d '*.example.com' -d example.com run +lego --email you@example.com --dns acme-dns --domains my.example.org run ``` @@ -45,21 +39,12 @@ lego --dns "acme-dns" -d '*.example.com' -d example.com run | Environment Variable Name | Description | |-----------------------|-------------| | `ACME_DNS_API_BASE` | The ACME-DNS API address | -| `ACME_DNS_STORAGE_BASE_URL` | The ACME-DNS JSON account data server. | | `ACME_DNS_STORAGE_PATH` | The ACME-DNS JSON account data file. A per-domain account will be registered/persisted to this file and used for TXT updates. | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `ACME_DNS_ALLOWLIST` | Source networks using CIDR notation (multiple values should be separated with a comma). | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). @@ -67,7 +52,7 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). ## More information - [API documentation](https://github.com/joohoi/acme-dns#api) -- [Go client](https://github.com/nrdcg/goacmedns) +- [Go client](https://github.com/cpu/goacmedns) diff --git a/docs/content/dns/zz_gen_active24.md b/docs/content/dns/zz_gen_active24.md deleted file mode 100644 index 6ec5c467a..000000000 --- a/docs/content/dns/zz_gen_active24.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: "Active24" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: active24 -dnsprovider: - since: "v4.23.0" - code: "active24" - url: "https://www.active24.cz" ---- - - - - - - -Configuration for [Active24](https://www.active24.cz). - - - - -- Code: `active24` -- Since: v4.23.0 - - -Here is an example bash command using the Active24 provider: - -```bash -ACTIVE24_API_KEY="xxx" \ -ACTIVE24_SECRET="yyy" \ -lego --dns active24 -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `ACTIVE24_API_KEY` | API key | -| `ACTIVE24_SECRET` | Secret | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `ACTIVE24_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `ACTIVE24_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `ACTIVE24_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `ACTIVE24_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://rest.active24.cz/v2/docs) - - - - diff --git a/docs/content/dns/zz_gen_alidns.md b/docs/content/dns/zz_gen_alidns.md index 4ded782ab..5a009adaa 100644 --- a/docs/content/dns/zz_gen_alidns.md +++ b/docs/content/dns/zz_gen_alidns.md @@ -28,13 +28,13 @@ Here is an example bash command using the Alibaba Cloud DNS provider: ```bash # Setup using instance RAM role ALICLOUD_RAM_ROLE=lego \ -lego --dns alidns -d '*.example.com' -d example.com run +lego --email you@example.com --dns alidns --domains my.example.org run # Or, using credentials ALICLOUD_ACCESS_KEY=abcdefghijklmnopqrstuvwx \ ALICLOUD_SECRET_KEY=your-secret-key \ ALICLOUD_SECURITY_TOKEN=your-sts-token \ -lego --dns alidns - -d '*.example.com' -d example.com run +lego --email you@example.com --dns alidns --domains my.example.org run ``` @@ -45,27 +45,25 @@ lego --dns alidns - -d '*.example.com' -d example.com run | Environment Variable Name | Description | |-----------------------|-------------| | `ALICLOUD_ACCESS_KEY` | Access key ID | -| `ALICLOUD_RAM_ROLE` | Your instance RAM role (https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance) | +| `ALICLOUD_RAM_ROLE` | Your instance RAM role (https://www.alibabacloud.com/help/doc-detail/54579.htm) | | `ALICLOUD_SECRET_KEY` | Access Key secret | | `ALICLOUD_SECURITY_TOKEN` | STS Security Token (optional) | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `ALICLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | -| `ALICLOUD_LINE` | Line (Default: default) | -| `ALICLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `ALICLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `ALICLOUD_REGION_ID` | Region ID (Default: cn-hangzhou) | -| `ALICLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) | +| `ALICLOUD_HTTP_TIMEOUT` | API request timeout | +| `ALICLOUD_POLLING_INTERVAL` | Time between DNS propagation check | +| `ALICLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `ALICLOUD_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). @@ -73,7 +71,7 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). ## More information - [API documentation](https://www.alibabacloud.com/help/en/alibaba-cloud-dns/latest/api-alidns-2015-01-09-dir-parsing-records) -- [Go client](https://github.com/alibabacloud-go/alidns-20150109) +- [Go client](https://github.com/aliyun/alibaba-cloud-sdk-go) diff --git a/docs/content/dns/zz_gen_aliesa.md b/docs/content/dns/zz_gen_aliesa.md deleted file mode 100644 index af28f9a4e..000000000 --- a/docs/content/dns/zz_gen_aliesa.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -title: "AlibabaCloud ESA" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: aliesa -dnsprovider: - since: "v4.29.0" - code: "aliesa" - url: "https://www.alibabacloud.com/en/product/esa" ---- - - - - - - -Configuration for [AlibabaCloud ESA](https://www.alibabacloud.com/en/product/esa). - - - - -- Code: `aliesa` -- Since: v4.29.0 - - -Here is an example bash command using the AlibabaCloud ESA provider: - -```bash -# Setup using instance RAM role -ALIESA_RAM_ROLE=lego \ -lego --dns aliesa -d '*.example.com' -d example.com run - -# Or, using credentials -ALIESA_ACCESS_KEY=abcdefghijklmnopqrstuvwx \ -ALIESA_SECRET_KEY=your-secret-key \ -ALIESA_SECURITY_TOKEN=your-sts-token \ -lego --dns aliesa - -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `ALIESA_ACCESS_KEY` | Access key ID | -| `ALIESA_RAM_ROLE` | Your instance RAM role (https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance) | -| `ALIESA_SECRET_KEY` | Access Key secret | -| `ALIESA_SECURITY_TOKEN` | STS Security Token (optional) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `ALIESA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `ALIESA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `ALIESA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `ALIESA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-overview?spm=a2c63.p38356.help-menu-2673927.d_6_0_0.20b224c28PSZDc#:~:text=DNS-,DNS%20records,-DNS%20records) -- [Go client](https://github.com/alibabacloud-go/esa-20240910) - - - - diff --git a/docs/content/dns/zz_gen_allinkl.md b/docs/content/dns/zz_gen_allinkl.md index 2db6ae2c5..05d8ce017 100644 --- a/docs/content/dns/zz_gen_allinkl.md +++ b/docs/content/dns/zz_gen_allinkl.md @@ -28,7 +28,7 @@ Here is an example bash command using the all-inkl provider: ```bash ALL_INKL_LOGIN=xxxxxxxxxxxxxxxxxxxxxxxxxx \ ALL_INKL_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \ -lego --dns allinkl -d '*.example.com' -d example.com run +lego --email you@example.com --dns allinkl --domains my.example.org run ``` @@ -42,19 +42,19 @@ lego --dns allinkl -d '*.example.com' -d example.com run | `ALL_INKL_PASSWORD` | KAS password | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `ALL_INKL_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `ALL_INKL_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `ALL_INKL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `ALL_INKL_HTTP_TIMEOUT` | API request timeout | +| `ALL_INKL_POLLING_INTERVAL` | Time between DNS propagation check | +| `ALL_INKL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_alwaysdata.md b/docs/content/dns/zz_gen_alwaysdata.md deleted file mode 100644 index 6ec332d16..000000000 --- a/docs/content/dns/zz_gen_alwaysdata.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: "Alwaysdata" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: alwaysdata -dnsprovider: - since: "v4.31.0" - code: "alwaysdata" - url: "https://alwaysdata.com/" ---- - - - - - - -Configuration for [Alwaysdata](https://alwaysdata.com/). - - - - -- Code: `alwaysdata` -- Since: v4.31.0 - - -Here is an example bash command using the Alwaysdata provider: - -```bash -ALWAYSDATA_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns alwaysdata -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `ALWAYSDATA_API_KEY` | API Key | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `ALWAYSDATA_ACCOUNT` | Account name | -| `ALWAYSDATA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `ALWAYSDATA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `ALWAYSDATA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `ALWAYSDATA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://help.alwaysdata.com/en/api/resources/) - - - - diff --git a/docs/content/dns/zz_gen_anexia.md b/docs/content/dns/zz_gen_anexia.md deleted file mode 100644 index e12ec7cfd..000000000 --- a/docs/content/dns/zz_gen_anexia.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -title: "Anexia CloudDNS" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: anexia -dnsprovider: - since: "v4.28.0" - code: "anexia" - url: "https://www.anexia-it.com/" ---- - - - - - - -Configuration for [Anexia CloudDNS](https://www.anexia-it.com/). - - - - -- Code: `anexia` -- Since: v4.28.0 - - -Here is an example bash command using the Anexia CloudDNS provider: - -```bash -ANEXIA_TOKEN=xxx \ -lego --dns anexia -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `ANEXIA_TOKEN` | API token for Anexia Engine | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `ANEXIA_API_URL` | API endpoint URL (default: https://engine.anexia-it.com) | -| `ANEXIA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `ANEXIA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `ANEXIA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) | -| `ANEXIA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - -## Description - -You need to create an API token in the [Anexia Engine](https://engine.anexia-it.com/). - -The token must have permissions to manage DNS zones and records. - - - -## More information - -- [API documentation](https://engine.anexia-it.com/docs/en/module/clouddns/api) - - - - diff --git a/docs/content/dns/zz_gen_artfiles.md b/docs/content/dns/zz_gen_artfiles.md deleted file mode 100644 index 15ac2d964..000000000 --- a/docs/content/dns/zz_gen_artfiles.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: "ArtFiles" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: artfiles -dnsprovider: - since: "v4.32.0" - code: "artfiles" - url: "https://www.artfiles.de/extras/domains/" ---- - - - - - - -Configuration for [ArtFiles](https://www.artfiles.de/extras/domains/). - - - - -- Code: `artfiles` -- Since: v4.32.0 - - -Here is an example bash command using the ArtFiles provider: - -```bash -ARTFILES_USERNAME="xxx" \ -ARTFILES_PASSWORD="yyy" \ -lego --dns artfiles -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `ARTFILES_PASSWORD` | API password | -| `ARTFILES_USERNAME` | API username | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `ARTFILES_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `ARTFILES_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `ARTFILES_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 360) | -| `ARTFILES_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://support.artfiles.de/DCP-API#dns) - - - - diff --git a/docs/content/dns/zz_gen_arvancloud.md b/docs/content/dns/zz_gen_arvancloud.md index 96d495f71..f39f8c517 100644 --- a/docs/content/dns/zz_gen_arvancloud.md +++ b/docs/content/dns/zz_gen_arvancloud.md @@ -27,7 +27,7 @@ Here is an example bash command using the ArvanCloud provider: ```bash ARVANCLOUD_API_KEY="Apikey xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \ -lego --dns arvancloud -d '*.example.com' -d example.com run +lego --email you@example.com --dns arvancloud --domains my.example.org run ``` @@ -40,20 +40,20 @@ lego --dns arvancloud -d '*.example.com' -d example.com run | `ARVANCLOUD_API_KEY` | API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `ARVANCLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `ARVANCLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `ARVANCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `ARVANCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) | +| `ARVANCLOUD_HTTP_TIMEOUT` | API request timeout | +| `ARVANCLOUD_POLLING_INTERVAL` | Time between DNS propagation check | +| `ARVANCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `ARVANCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_auroradns.md b/docs/content/dns/zz_gen_auroradns.md index d608c85bb..326e6113d 100644 --- a/docs/content/dns/zz_gen_auroradns.md +++ b/docs/content/dns/zz_gen_auroradns.md @@ -28,7 +28,7 @@ Here is an example bash command using the Aurora DNS provider: ```bash AURORA_API_KEY=xxxxx \ AURORA_SECRET=yyyyyy \ -lego --dns auroradns -d '*.example.com' -d example.com run +lego --email you@example.com --dns auroradns --domains my.example.org run ``` @@ -42,7 +42,7 @@ lego --dns auroradns -d '*.example.com' -d example.com run | `AURORA_SECRET` | Secret password to be used | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -50,12 +50,12 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | Environment Variable Name | Description | |--------------------------------|-------------| | `AURORA_ENDPOINT` | API endpoint URL | -| `AURORA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `AURORA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `AURORA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `AURORA_POLLING_INTERVAL` | Time between DNS propagation check | +| `AURORA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `AURORA_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_autodns.md b/docs/content/dns/zz_gen_autodns.md index f1f25e916..167dea83f 100644 --- a/docs/content/dns/zz_gen_autodns.md +++ b/docs/content/dns/zz_gen_autodns.md @@ -28,7 +28,7 @@ Here is an example bash command using the Autodns provider: ```bash AUTODNS_API_USER=username \ AUTODNS_API_PASSWORD=supersecretpassword \ -lego --dns autodns -d '*.example.com' -d example.com run +lego --email you@example.com --dns autodns --domains my.example.org run ``` @@ -42,7 +42,7 @@ lego --dns autodns -d '*.example.com' -d example.com run | `AUTODNS_API_USER` | Username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -51,13 +51,13 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). |--------------------------------|-------------| | `AUTODNS_CONTEXT` | API context (4 for production, 1 for testing. Defaults to 4) | | `AUTODNS_ENDPOINT` | API endpoint URL, defaults to https://api.autodns.com/v1/ | -| `AUTODNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `AUTODNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `AUTODNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `AUTODNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) | +| `AUTODNS_HTTP_TIMEOUT` | API request timeout, defaults to 30 seconds | +| `AUTODNS_POLLING_INTERVAL` | Time between DNS propagation check | +| `AUTODNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `AUTODNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_axelname.md b/docs/content/dns/zz_gen_axelname.md deleted file mode 100644 index 91476e521..000000000 --- a/docs/content/dns/zz_gen_axelname.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: "Axelname" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: axelname -dnsprovider: - since: "v4.23.0" - code: "axelname" - url: "https://axelname.ru" ---- - - - - - - -Configuration for [Axelname](https://axelname.ru). - - - - -- Code: `axelname` -- Since: v4.23.0 - - -Here is an example bash command using the Axelname provider: - -```bash -AXELNAME_NICKNAME="yyy" \ -AXELNAME_TOKEN="xxx" \ -lego --dns axelname -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `AXELNAME_NICKNAME` | Account nickname | -| `AXELNAME_TOKEN` | API token | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `AXELNAME_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `AXELNAME_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `AXELNAME_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `AXELNAME_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://axelname.ru/static/content/files/axelname_api_rest_lite.pdf) - - - - diff --git a/docs/content/dns/zz_gen_azion.md b/docs/content/dns/zz_gen_azion.md deleted file mode 100644 index c5ca33552..000000000 --- a/docs/content/dns/zz_gen_azion.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: "Azion" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: azion -dnsprovider: - since: "v4.24.0" - code: "azion" - url: "https://www.azion.com/en/products/edge-dns/" ---- - - - - - - -Configuration for [Azion](https://www.azion.com/en/products/edge-dns/). - - - - -- Code: `azion` -- Since: v4.24.0 - - -Here is an example bash command using the Azion provider: - -```bash -AZION_PERSONAL_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --dns azion -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `AZION_PERSONAL_TOKEN` | Your Azion personal token. | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `AZION_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `AZION_PAGE_SIZE` | The page size for the API request (Default: 50) | -| `AZION_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `AZION_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `AZION_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://api.azion.com/) -- [Go client](https://github.com/aziontech/azionapi-go-sdk) - - - - diff --git a/docs/content/dns/zz_gen_azure.md b/docs/content/dns/zz_gen_azure.md index 5063c202f..399bec020 100644 --- a/docs/content/dns/zz_gen_azure.md +++ b/docs/content/dns/zz_gen_azure.md @@ -43,7 +43,7 @@ _Please contribute by adding a CLI example._ | `instance metadata service` | If the credentials are **not** set via the environment, then it will attempt to get a bearer token via the [instance metadata service](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service). | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -51,14 +51,14 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | Environment Variable Name | Description | |--------------------------------|-------------| | `AZURE_METADATA_ENDPOINT` | Metadata Service endpoint URL | -| `AZURE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `AZURE_POLLING_INTERVAL` | Time between DNS propagation check | | `AZURE_PRIVATE_ZONE` | Set to true to use Azure Private DNS Zones and not public | -| `AZURE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `AZURE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | +| `AZURE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `AZURE_TTL` | The TTL of the TXT record used for the DNS challenge | | `AZURE_ZONE_NAME` | Zone name to use inside Azure DNS service to add the TXT record in | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_azuredns.md b/docs/content/dns/zz_gen_azuredns.md index 3b2586711..da7f2a29a 100644 --- a/docs/content/dns/zz_gen_azuredns.md +++ b/docs/content/dns/zz_gen_azuredns.md @@ -31,32 +31,32 @@ Here is an example bash command using the Azure DNS provider: AZURE_CLIENT_ID= \ AZURE_TENANT_ID= \ AZURE_CLIENT_SECRET= \ -lego --dns azuredns -d '*.example.com' -d example.com run +lego --domains example.com --email your_example@email.com --dns azuredns run ### Using client certificate AZURE_CLIENT_ID= \ AZURE_TENANT_ID= \ AZURE_CLIENT_CERTIFICATE_PATH= \ -lego --dns azuredns -d '*.example.com' -d example.com run +lego --domains example.com --email your_example@email.com --dns azuredns run ### Using Azure CLI az login \ -lego --dns azuredns -d '*.example.com' -d example.com run +lego --domains example.com --email your_example@email.com --dns azuredns run ### Using Managed Identity (Azure VM) AZURE_TENANT_ID= \ AZURE_RESOURCE_GROUP= \ -lego --dns azuredns -d '*.example.com' -d example.com run +lego --domains example.com --email your_example@email.com --dns azuredns run ### Using Managed Identity (Azure Arc) AZURE_TENANT_ID= \ IMDS_ENDPOINT=http://localhost:40342 \ IDENTITY_ENDPOINT=http://localhost:40342/metadata/identity/oauth2/token \ -lego --dns azuredns -d '*.example.com' -d example.com run +lego --domains example.com --email your_example@email.com --dns azuredns run ``` @@ -73,7 +73,7 @@ lego --dns azuredns -d '*.example.com' -d example.com run | `AZURE_TENANT_ID` | Tenant ID | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -83,17 +83,17 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | `AZURE_AUTH_METHOD` | Specify which authentication method to use | | `AZURE_AUTH_MSI_TIMEOUT` | Managed Identity timeout duration | | `AZURE_ENVIRONMENT` | Azure environment, one of: public, usgovernment, and china | -| `AZURE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `AZURE_POLLING_INTERVAL` | Time between DNS propagation check | | `AZURE_PRIVATE_ZONE` | Set to true to use Azure Private DNS Zones and not public | -| `AZURE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | +| `AZURE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `AZURE_RESOURCE_GROUP` | DNS zone resource group | | `AZURE_SERVICEDISCOVERY_FILTER` | Advanced ServiceDiscovery filter using Kusto query condition | | `AZURE_SUBSCRIPTION_ID` | DNS zone subscription ID | -| `AZURE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | +| `AZURE_TTL` | The TTL of the TXT record used for the DNS challenge | | `AZURE_ZONE_NAME` | Zone name to use inside Azure DNS service to add the TXT record in | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Description @@ -229,10 +229,6 @@ This authentication method can be specifically used by setting the `AZURE_AUTH_M Open ID Connect is a mechanism that establish a trust relationship between a running environment and the Azure AD identity provider. It can be enabled by setting the `AZURE_AUTH_METHOD` environment variable to `oidc`. -### Azure DevOps Pipelines - -It can be enabled by setting the `AZURE_AUTH_METHOD` environment variable to `pipeline`. - diff --git a/docs/content/dns/zz_gen_baiducloud.md b/docs/content/dns/zz_gen_baiducloud.md deleted file mode 100644 index 59a2f9a2d..000000000 --- a/docs/content/dns/zz_gen_baiducloud.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: "Baidu Cloud" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: baiducloud -dnsprovider: - since: "v4.23.0" - code: "baiducloud" - url: "https://cloud.baidu.com" ---- - - - - - - -Configuration for [Baidu Cloud](https://cloud.baidu.com). - - - - -- Code: `baiducloud` -- Since: v4.23.0 - - -Here is an example bash command using the Baidu Cloud provider: - -```bash -BAIDUCLOUD_ACCESS_KEY_ID="xxx" \ -BAIDUCLOUD_SECRET_ACCESS_KEY="yyy" \ -lego --dns baiducloud -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `BAIDUCLOUD_ACCESS_KEY_ID` | Access key | -| `BAIDUCLOUD_SECRET_ACCESS_KEY` | Secret access key | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `BAIDUCLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `BAIDUCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `BAIDUCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://cloud.baidu.com/doc/DNS/s/El4s7lssr) -- [Go client](https://github.com/baidubce/bce-sdk-go) - - - - diff --git a/docs/content/dns/zz_gen_beget.md b/docs/content/dns/zz_gen_beget.md deleted file mode 100644 index 3f03a2ac5..000000000 --- a/docs/content/dns/zz_gen_beget.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: "Beget.com" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: beget -dnsprovider: - since: "v4.27.0" - code: "beget" - url: "https://beget.com/" ---- - - - - - - -Configuration for [Beget.com](https://beget.com/). - - - - -- Code: `beget` -- Since: v4.27.0 - - -Here is an example bash command using the Beget.com provider: - -```bash -BEGET_USERNAME=xxxxxx \ -BEGET_PASSWORD=yyyyyy \ -lego --dns beget -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `BEGET_PASSWORD` | API password | -| `BEGET_USERNAME` | API username | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `BEGET_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `BEGET_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 30) | -| `BEGET_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) | -| `BEGET_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://beget.com/ru/kb/api/funkczii-upravleniya-dns) - - - - diff --git a/docs/content/dns/zz_gen_binarylane.md b/docs/content/dns/zz_gen_binarylane.md deleted file mode 100644 index eebf3c54e..000000000 --- a/docs/content/dns/zz_gen_binarylane.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: "Binary Lane" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: binarylane -dnsprovider: - since: "v4.26.0" - code: "binarylane" - url: "https://www.binarylane.com.au/" ---- - - - - - - -Configuration for [Binary Lane](https://www.binarylane.com.au/). - - - - -- Code: `binarylane` -- Since: v4.26.0 - - -Here is an example bash command using the Binary Lane provider: - -```bash -BINARYLANE_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns binarylane -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `BINARYLANE_API_TOKEN` | API token | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `BINARYLANE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `BINARYLANE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `BINARYLANE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `BINARYLANE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://api.binarylane.com.au/reference/#tag/Domains) - - - - diff --git a/docs/content/dns/zz_gen_bindman.md b/docs/content/dns/zz_gen_bindman.md index fcceb8962..c83147ff7 100644 --- a/docs/content/dns/zz_gen_bindman.md +++ b/docs/content/dns/zz_gen_bindman.md @@ -27,7 +27,7 @@ Here is an example bash command using the Bindman provider: ```bash BINDMAN_MANAGER_ADDRESS= \ -lego --dns bindman -d '*.example.com' -d example.com run +lego --email you@example.com --dns bindman --domains my.example.org run ``` @@ -40,19 +40,19 @@ lego --dns bindman -d '*.example.com' -d example.com run | `BINDMAN_MANAGER_ADDRESS` | The server URL, should have scheme, hostname, and port (if required) of the Bindman-DNS Manager server | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `BINDMAN_HTTP_TIMEOUT` | API request timeout in seconds (Default: 60) | -| `BINDMAN_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `BINDMAN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `BINDMAN_HTTP_TIMEOUT` | API request timeout | +| `BINDMAN_POLLING_INTERVAL` | Time between DNS propagation check | +| `BINDMAN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_bluecat.md b/docs/content/dns/zz_gen_bluecat.md index 2d9eb5b48..80a8fb8ac 100644 --- a/docs/content/dns/zz_gen_bluecat.md +++ b/docs/content/dns/zz_gen_bluecat.md @@ -32,7 +32,7 @@ BLUECAT_USER_NAME=myusername \ BLUECAT_CONFIG_NAME=myconfig \ BLUECAT_SERVER_URL=https://bam.example.com \ BLUECAT_TTL=30 \ -lego --dns bluecat -d '*.example.com' -d example.com run +lego --email you@example.com --dns bluecat --domains my.example.org run ``` @@ -49,21 +49,20 @@ lego --dns bluecat -d '*.example.com' -d example.com run | `BLUECAT_USER_NAME` | API username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `BLUECAT_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `BLUECAT_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `BLUECAT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `BLUECAT_SKIP_DEPLOY` | Skip deployements | -| `BLUECAT_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `BLUECAT_HTTP_TIMEOUT` | API request timeout | +| `BLUECAT_POLLING_INTERVAL` | Time between DNS propagation check | +| `BLUECAT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `BLUECAT_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_bluecatv2.md b/docs/content/dns/zz_gen_bluecatv2.md deleted file mode 100644 index 7d748df99..000000000 --- a/docs/content/dns/zz_gen_bluecatv2.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: "Bluecat v2" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: bluecatv2 -dnsprovider: - since: "v4.32.0" - code: "bluecatv2" - url: "https://www.bluecatnetworks.com" ---- - - - - - - -Configuration for [Bluecat v2](https://www.bluecatnetworks.com). - - - - -- Code: `bluecatv2` -- Since: v4.32.0 - - -Here is an example bash command using the Bluecat v2 provider: - -```bash -BLUECATV2_SERVER_URL="https://example.com" \ -BLUECATV2_USERNAME="xxx" \ -BLUECATV2_PASSWORD="yyy" \ -BLUECATV2_CONFIG_NAME="myConfiguration" \ -BLUECATV2_VIEW_NAME="myView" \ -lego --dns bluecatv2 -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `BLUECATV2_CONFIG_NAME` | Configuration name | -| `BLUECATV2_PASSWORD` | API password | -| `BLUECATV2_USERNAME` | API username | -| `BLUECATV2_VIEW_NAME` | DNS View Name | -| `BLUECAT_SERVER_URL` | The server URL: it should have a scheme, hostname, and port (if required) of the authoritative Bluecat BAM serve | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `BLUECATV2_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `BLUECATV2_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `BLUECATV2_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `BLUECATV2_SKIP_DEPLOY` | Skip quick deployements | -| `BLUECATV2_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Introduction/9.6.0) - - - - diff --git a/docs/content/dns/zz_gen_bookmyname.md b/docs/content/dns/zz_gen_bookmyname.md deleted file mode 100644 index cb7e1d3a1..000000000 --- a/docs/content/dns/zz_gen_bookmyname.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: "BookMyName" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: bookmyname -dnsprovider: - since: "v4.23.0" - code: "bookmyname" - url: "https://www.bookmyname.com/" ---- - - - - - - -Configuration for [BookMyName](https://www.bookmyname.com/). - - - - -- Code: `bookmyname` -- Since: v4.23.0 - - -Here is an example bash command using the BookMyName provider: - -```bash -BOOKMYNAME_USERNAME="xxx" \ -BOOKMYNAME_PASSWORD="yyy" \ -lego --dns bookmyname -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `BOOKMYNAME_PASSWORD` | Password | -| `BOOKMYNAME_USERNAME` | Username | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `BOOKMYNAME_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `BOOKMYNAME_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `BOOKMYNAME_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `BOOKMYNAME_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://fr.faqs.bookmyname.com/frfaqs/dyndns) - - - - diff --git a/docs/content/dns/zz_gen_brandit.md b/docs/content/dns/zz_gen_brandit.md index fdb538684..237c02af1 100644 --- a/docs/content/dns/zz_gen_brandit.md +++ b/docs/content/dns/zz_gen_brandit.md @@ -1,5 +1,5 @@ --- -title: "Brandit (deprecated)" +title: "Brandit" date: 2019-03-03T16:39:46+01:00 draft: false slug: brandit @@ -13,11 +13,8 @@ dnsprovider: -Brandit has been acquired by Abion. -Abion has a different API. - -If you are a Brandit/Albion user, you can try the PR https://github.com/go-acme/lego/pull/2112. +Configuration for [Brandit](https://www.brandit.com/). @@ -26,12 +23,12 @@ If you are a Brandit/Albion user, you can try the PR https://github.com/go-acme/ - Since: v4.11.0 -Here is an example bash command using the Brandit (deprecated) provider: +Here is an example bash command using the Brandit provider: ```bash BRANDIT_API_KEY=xxxxxxxxxxxxxxxxxxxxx \ BRANDIT_API_USERNAME=yyyyyyyyyyyyyyyyyyyy \ -lego --dns brandit -d '*.example.com' -d example.com run +lego --email myemail@example.com --dns brandit --domains my.example.org run ``` @@ -45,20 +42,20 @@ lego --dns brandit -d '*.example.com' -d example.com run | `BRANDIT_API_USERNAME` | The API username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `BRANDIT_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `BRANDIT_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `BRANDIT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 600) | -| `BRANDIT_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) | +| `BRANDIT_HTTP_TIMEOUT` | API request timeout | +| `BRANDIT_POLLING_INTERVAL` | Time between DNS propagation check | +| `BRANDIT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `BRANDIT_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_bunny.md b/docs/content/dns/zz_gen_bunny.md index 63c30782a..d157f895f 100644 --- a/docs/content/dns/zz_gen_bunny.md +++ b/docs/content/dns/zz_gen_bunny.md @@ -27,7 +27,7 @@ Here is an example bash command using the Bunny provider: ```bash BUNNY_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ -lego --dns bunny -d '*.example.com' -d example.com run +lego --email you@example.com --dns bunny --domains my.example.org run ``` @@ -40,20 +40,19 @@ lego --dns bunny -d '*.example.com' -d example.com run | `BUNNY_API_KEY` | API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `BUNNY_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `BUNNY_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `BUNNY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `BUNNY_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | +| `BUNNY_POLLING_INTERVAL` | Time between DNS propagation check | +| `BUNNY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `BUNNY_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_checkdomain.md b/docs/content/dns/zz_gen_checkdomain.md index e0275f6c9..4f990170e 100644 --- a/docs/content/dns/zz_gen_checkdomain.md +++ b/docs/content/dns/zz_gen_checkdomain.md @@ -27,7 +27,7 @@ Here is an example bash command using the Checkdomain provider: ```bash CHECKDOMAIN_TOKEN=yoursecrettoken \ -lego --dns checkdomain -d '*.example.com' -d example.com run +lego --email you@example.com --dns checkdomain --domains my.example.org run ``` @@ -40,7 +40,7 @@ lego --dns checkdomain -d '*.example.com' -d example.com run | `CHECKDOMAIN_TOKEN` | API token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -48,13 +48,13 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | Environment Variable Name | Description | |--------------------------------|-------------| | `CHECKDOMAIN_ENDPOINT` | API endpoint URL, defaults to https://api.checkdomain.de | -| `CHECKDOMAIN_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `CHECKDOMAIN_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 300) | -| `CHECKDOMAIN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 7) | -| `CHECKDOMAIN_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `CHECKDOMAIN_HTTP_TIMEOUT` | API request timeout, defaults to 30 seconds | +| `CHECKDOMAIN_POLLING_INTERVAL` | Time between DNS propagation check | +| `CHECKDOMAIN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `CHECKDOMAIN_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_civo.md b/docs/content/dns/zz_gen_civo.md index 61303b539..af0278fc1 100644 --- a/docs/content/dns/zz_gen_civo.md +++ b/docs/content/dns/zz_gen_civo.md @@ -27,7 +27,7 @@ Here is an example bash command using the Civo provider: ```bash CIVO_TOKEN=xxxxxx \ -lego --dns civo -d '*.example.com' -d example.com run +lego --email you@example.com --dns civo --domains my.example.org run ``` @@ -40,19 +40,19 @@ lego --dns civo -d '*.example.com' -d example.com run | `CIVO_TOKEN` | Authentication token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `CIVO_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 30) | -| `CIVO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) | -| `CIVO_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) | +| `CIVO_POLLING_INTERVAL` | Time between DNS propagation check | +| `CIVO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `CIVO_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_clouddns.md b/docs/content/dns/zz_gen_clouddns.md index d10d1d6a1..839b745fc 100644 --- a/docs/content/dns/zz_gen_clouddns.md +++ b/docs/content/dns/zz_gen_clouddns.md @@ -29,7 +29,7 @@ Here is an example bash command using the CloudDNS provider: CLOUDDNS_CLIENT_ID=bLsdFAks23429841238feb177a572aX \ CLOUDDNS_EMAIL=you@example.com \ CLOUDDNS_PASSWORD=b9841238feb177a84330f \ -lego --dns clouddns -d '*.example.com' -d example.com run +lego --email you@example.com --dns clouddns --domains my.example.org run ``` @@ -44,20 +44,20 @@ lego --dns clouddns -d '*.example.com' -d example.com run | `CLOUDDNS_PASSWORD` | Account password | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `CLOUDDNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `CLOUDDNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 5) | -| `CLOUDDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `CLOUDDNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `CLOUDDNS_HTTP_TIMEOUT` | API request timeout | +| `CLOUDDNS_POLLING_INTERVAL` | Time between DNS propagation check | +| `CLOUDDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `CLOUDDNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_cloudflare.md b/docs/content/dns/zz_gen_cloudflare.md index f3390a5fd..43b197c40 100644 --- a/docs/content/dns/zz_gen_cloudflare.md +++ b/docs/content/dns/zz_gen_cloudflare.md @@ -28,12 +28,12 @@ Here is an example bash command using the Cloudflare provider: ```bash CLOUDFLARE_EMAIL=you@example.com \ CLOUDFLARE_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \ -lego --dns cloudflare -d '*.example.com' -d example.com run +lego --email you@example.com --dns cloudflare --domains my.example.org run # or CLOUDFLARE_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \ -lego --dns cloudflare -d '*.example.com' -d example.com run +lego --email you@example.com --dns cloudflare --domains my.example.org run ``` @@ -53,21 +53,20 @@ lego --dns cloudflare -d '*.example.com' -d example.com run | `CLOUDFLARE_ZONE_API_TOKEN` | Alias to CF_ZONE_API_TOKEN | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `CLOUDFLARE_BASE_URL` | API base URL (Default: https://api.cloudflare.com/client/v4) | -| `CLOUDFLARE_HTTP_TIMEOUT` | API request timeout in seconds (Default: ) | -| `CLOUDFLARE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `CLOUDFLARE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `CLOUDFLARE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `CLOUDFLARE_HTTP_TIMEOUT` | API request timeout | +| `CLOUDFLARE_POLLING_INTERVAL` | Time between DNS propagation check | +| `CLOUDFLARE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `CLOUDFLARE_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Description @@ -99,13 +98,12 @@ Then pass the API token as `CF_DNS_API_TOKEN` to Lego. **Alternatively,** if you prefer a more strict set of privileges, you can split the access tokens: -* Create one with *Zone / Zone / Read* permissions and scope it to all your zones or just the individual zone you need to edit. +* Create one with *Zone / Zone / Read* permissions and scope it to all your zones. This is needed to resolve domain names to Zone IDs and can be shared among multiple Lego installations. Pass this API token as `CF_ZONE_API_TOKEN` to Lego. * Create another API token with *Zone / DNS / Edit* permissions and set the scope to the domains you want to manage with a single Lego installation. Pass this token as `CF_DNS_API_TOKEN` to Lego. * Repeat the previous step for each host you want to run Lego on. -* It is possible to use the same api token for both variables if it is given `Zone:Read` and `DNS:Edit` permission for the zone. This "paranoid" setup is mainly interesting for users who manage many zones/domains with a single Cloudflare account. It follows the principle of least privilege and limits the possible damage, should one of the hosts become compromised. diff --git a/docs/content/dns/zz_gen_cloudns.md b/docs/content/dns/zz_gen_cloudns.md index 26bd838f2..32592e617 100644 --- a/docs/content/dns/zz_gen_cloudns.md +++ b/docs/content/dns/zz_gen_cloudns.md @@ -28,7 +28,7 @@ Here is an example bash command using the ClouDNS provider: ```bash CLOUDNS_AUTH_ID=xxxx \ CLOUDNS_AUTH_PASSWORD=yyyy \ -lego --dns cloudns -d '*.example.com' -d example.com run +lego --email you@example.com --dns cloudns --domains my.example.org run ``` @@ -42,21 +42,21 @@ lego --dns cloudns -d '*.example.com' -d example.com run | `CLOUDNS_AUTH_PASSWORD` | The password for API user ID | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `CLOUDNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `CLOUDNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | -| `CLOUDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 180) | +| `CLOUDNS_HTTP_TIMEOUT` | API request timeout | +| `CLOUDNS_POLLING_INTERVAL` | Time between DNS propagation check | +| `CLOUDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `CLOUDNS_SUB_AUTH_ID` | The API sub user ID | -| `CLOUDNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | +| `CLOUDNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_cloudru.md b/docs/content/dns/zz_gen_cloudru.md index 6dc3b0030..86e281f21 100644 --- a/docs/content/dns/zz_gen_cloudru.md +++ b/docs/content/dns/zz_gen_cloudru.md @@ -29,7 +29,7 @@ Here is an example bash command using the Cloud.ru provider: CLOUDRU_SERVICE_INSTANCE_ID=ppp \ CLOUDRU_KEY_ID=xxx \ CLOUDRU_SECRET=yyy \ -lego --dns cloudru -d '*.example.com' -d example.com run +lego --email you@example.com --dns cloudru --domains my.example.org run ``` @@ -44,21 +44,21 @@ lego --dns cloudru -d '*.example.com' -d example.com run | `CLOUDRU_SERVICE_INSTANCE_ID` | Service Instance ID (parentId) | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `CLOUDRU_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `CLOUDRU_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 5) | -| `CLOUDRU_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) | -| `CLOUDRU_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 120) | -| `CLOUDRU_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `CLOUDRU_HTTP_TIMEOUT` | API request timeout | +| `CLOUDRU_POLLING_INTERVAL` | Time between DNS propagation check | +| `CLOUDRU_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `CLOUDRU_SEQUENCE_INTERVAL` | Time between sequential requests | +| `CLOUDRU_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_cloudxns.md b/docs/content/dns/zz_gen_cloudxns.md index b26e5ddb5..00d7a08bf 100644 --- a/docs/content/dns/zz_gen_cloudxns.md +++ b/docs/content/dns/zz_gen_cloudxns.md @@ -1,20 +1,20 @@ --- -title: "CloudXNS (Deprecated)" +title: "CloudXNS" date: 2019-03-03T16:39:46+01:00 draft: false slug: cloudxns dnsprovider: since: "v0.5.0" code: "cloudxns" - url: "https://github.com/go-acme/lego/issues/2323" + url: "https://www.cloudxns.net/" --- -The CloudXNS DNS provider has shut down. +Configuration for [CloudXNS](https://www.cloudxns.net/). @@ -23,12 +23,12 @@ The CloudXNS DNS provider has shut down. - Since: v0.5.0 -Here is an example bash command using the CloudXNS (Deprecated) provider: +Here is an example bash command using the CloudXNS provider: ```bash CLOUDXNS_API_KEY=xxxx \ CLOUDXNS_SECRET_KEY=yyyy \ -lego --dns cloudxns -d '*.example.com' -d example.com run +lego --email you@example.com --dns cloudxns --domains my.example.org run ``` @@ -42,24 +42,27 @@ lego --dns cloudxns -d '*.example.com' -d example.com run | `CLOUDXNS_SECRET_KEY` | The API secret key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `CLOUDXNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: ) | -| `CLOUDXNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: ) | -| `CLOUDXNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: ) | -| `CLOUDXNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: ) | +| `CLOUDXNS_HTTP_TIMEOUT` | API request timeout | +| `CLOUDXNS_POLLING_INTERVAL` | Time between DNS propagation check | +| `CLOUDXNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `CLOUDXNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). +## More information + +- [API documentation](https://www.cloudxns.net/Public/Doc/CloudXNS_api2.0_doc_zh-cn.zip) diff --git a/docs/content/dns/zz_gen_com35.md b/docs/content/dns/zz_gen_com35.md deleted file mode 100644 index e2552e57c..000000000 --- a/docs/content/dns/zz_gen_com35.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: "35.com/三五互联" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: com35 -dnsprovider: - since: "v4.31.0" - code: "com35" - url: "https://www.35.cn/" ---- - - - - - - -Configuration for [35.com/三五互联](https://www.35.cn/). - - - - -- Code: `com35` -- Since: v4.31.0 - - -Here is an example bash command using the 35.com/三五互联 provider: - -```bash -COM35_USERNAME="xxx" \ -COM35_PASSWORD="yyy" \ -lego --dns com35 -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `COM35_PASSWORD` | API password | -| `COM35_USERNAME` | Username | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `COM35_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `COM35_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | -| `COM35_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `COM35_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://api.35.cn/CustomerCenter/doc/domain_v2.html) - - - - diff --git a/docs/content/dns/zz_gen_conoha.md b/docs/content/dns/zz_gen_conoha.md index 08a979b31..a24fef2d9 100644 --- a/docs/content/dns/zz_gen_conoha.md +++ b/docs/content/dns/zz_gen_conoha.md @@ -1,5 +1,5 @@ --- -title: "ConoHa v2" +title: "ConoHa" date: 2019-03-03T16:39:46+01:00 draft: false slug: conoha @@ -14,7 +14,7 @@ dnsprovider: -Configuration for [ConoHa v2](https://www.conoha.jp/). +Configuration for [ConoHa](https://www.conoha.jp/). @@ -23,13 +23,13 @@ Configuration for [ConoHa v2](https://www.conoha.jp/). - Since: v1.2.0 -Here is an example bash command using the ConoHa v2 provider: +Here is an example bash command using the ConoHa provider: ```bash CONOHA_TENANT_ID=487727e3921d44e3bfe7ebb337bf085e \ CONOHA_API_USERNAME=xxxx \ CONOHA_API_PASSWORD=yyyy \ -lego --dns conoha -d '*.example.com' -d example.com run +lego --email you@example.com --dns conoha --domains my.example.org run ``` @@ -44,28 +44,28 @@ lego --dns conoha -d '*.example.com' -d example.com run | `CONOHA_TENANT_ID` | Tenant ID | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `CONOHA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `CONOHA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `CONOHA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `CONOHA_REGION` | The region (Default: tyo1) | -| `CONOHA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | +| `CONOHA_HTTP_TIMEOUT` | API request timeout | +| `CONOHA_POLLING_INTERVAL` | Time between DNS propagation check | +| `CONOHA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `CONOHA_REGION` | The region | +| `CONOHA_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information -- [API documentation](https://doc.conoha.jp/reference/api-vps2/api-dns-vps2) +- [API documentation](https://www.conoha.jp/docs/) diff --git a/docs/content/dns/zz_gen_conohav3.md b/docs/content/dns/zz_gen_conohav3.md deleted file mode 100644 index e473f9434..000000000 --- a/docs/content/dns/zz_gen_conohav3.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: "ConoHa v3" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: conohav3 -dnsprovider: - since: "v4.24.0" - code: "conohav3" - url: "https://www.conoha.jp/" ---- - - - - - - -Configuration for [ConoHa v3](https://www.conoha.jp/). - - - - -- Code: `conohav3` -- Since: v4.24.0 - - -Here is an example bash command using the ConoHa v3 provider: - -```bash -CONOHAV3_TENANT_ID=487727e3921d44e3bfe7ebb337bf085e \ -CONOHAV3_API_USER_ID=xxxx \ -CONOHAV3_API_PASSWORD=yyyy \ -lego --dns conohav3 -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `CONOHAV3_API_PASSWORD` | The API password | -| `CONOHAV3_API_USER_ID` | The API user ID | -| `CONOHAV3_TENANT_ID` | Tenant ID | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `CONOHAV3_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `CONOHAV3_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `CONOHAV3_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `CONOHAV3_REGION` | The region (Default: c3j1) | -| `CONOHAV3_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://doc.conoha.jp/reference/api-vps3/api-dns-vps3/) - - - - diff --git a/docs/content/dns/zz_gen_constellix.md b/docs/content/dns/zz_gen_constellix.md index d4ce02bac..6375b25c9 100644 --- a/docs/content/dns/zz_gen_constellix.md +++ b/docs/content/dns/zz_gen_constellix.md @@ -28,7 +28,7 @@ Here is an example bash command using the Constellix provider: ```bash CONSTELLIX_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ CONSTELLIX_SECRET_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ -lego --dns constellix -d '*.example.com' -d example.com run +lego --email you@example.com --dns constellix --domains my.example.org run ``` @@ -42,20 +42,20 @@ lego --dns constellix -d '*.example.com' -d example.com run | `CONSTELLIX_SECRET_KEY` | User secret key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `CONSTELLIX_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `CONSTELLIX_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | -| `CONSTELLIX_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `CONSTELLIX_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | +| `CONSTELLIX_HTTP_TIMEOUT` | API request timeout | +| `CONSTELLIX_POLLING_INTERVAL` | Time between DNS propagation check | +| `CONSTELLIX_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `CONSTELLIX_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_corenetworks.md b/docs/content/dns/zz_gen_corenetworks.md deleted file mode 100644 index 05468b1a3..000000000 --- a/docs/content/dns/zz_gen_corenetworks.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: "Core-Networks" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: corenetworks -dnsprovider: - since: "v4.20.0" - code: "corenetworks" - url: "https://www.core-networks.de/" ---- - - - - - - -Configuration for [Core-Networks](https://www.core-networks.de/). - - - - -- Code: `corenetworks` -- Since: v4.20.0 - - -Here is an example bash command using the Core-Networks provider: - -```bash -CORENETWORKS_LOGIN="xxxx" \ -CORENETWORKS_PASSWORD="yyyy" \ -lego --dns corenetworks -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `CORENETWORKS_LOGIN` | The username of the API account | -| `CORENETWORKS_PASSWORD` | The password | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `CORENETWORKS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `CORENETWORKS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `CORENETWORKS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `CORENETWORKS_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) | -| `CORENETWORKS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://beta.api.core-networks.de/doc/) - - - - diff --git a/docs/content/dns/zz_gen_cpanel.md b/docs/content/dns/zz_gen_cpanel.md index e5c0cc047..a9c3d61d3 100644 --- a/docs/content/dns/zz_gen_cpanel.md +++ b/docs/content/dns/zz_gen_cpanel.md @@ -28,18 +28,18 @@ Here is an example bash command using the CPanel/WHM provider: ```bash ### CPANEL (default) -CPANEL_USERNAME="yyyy" \ -CPANEL_TOKEN="xxxx" \ -CPANEL_BASE_URL="https://example.com:2083" \ -lego --dns cpanel -d '*.example.com' -d example.com run +CPANEL_USERNAME = "yyyy" +CPANEL_TOKEN = "xxxx" +CPANEL_BASE_URL = "https://example.com:2083" \ +lego --email you@example.com --dns cpanel --domains my.example.org run ## WHM -CPANEL_MODE=whm \ -CPANEL_USERNAME="yyyy" \ -CPANEL_TOKEN="xxxx" \ -CPANEL_BASE_URL="https://example.com:2087" \ -lego --dns cpanel -d '*.example.com' -d example.com run +CPANEL_MODE = whm +CPANEL_USERNAME = "yyyy" +CPANEL_TOKEN = "xxxx" +CPANEL_BASE_URL = "https://example.com:2087" \ +lego --email you@example.com --dns cpanel --domains my.example.org run ``` @@ -54,21 +54,22 @@ lego --dns cpanel -d '*.example.com' -d example.com run | `CPANEL_USERNAME` | username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `CPANEL_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `CPANEL_HTTP_TIMEOUT` | API request timeout | | `CPANEL_MODE` | use cpanel API or WHM API (Default: cpanel) | -| `CPANEL_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `CPANEL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `CPANEL_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `CPANEL_POLLING_INTERVAL` | Time between DNS propagation check | +| `CPANEL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `CPANEL_REGION` | The region | +| `CPANEL_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_czechia.md b/docs/content/dns/zz_gen_czechia.md deleted file mode 100644 index 7b1cdd1ae..000000000 --- a/docs/content/dns/zz_gen_czechia.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: "Czechia" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: czechia -dnsprovider: - since: "v4.33.0" - code: "czechia" - url: "https://www.czechia.com/" ---- - - - - - - -Configuration for [Czechia](https://www.czechia.com/). - - - - -- Code: `czechia` -- Since: v4.33.0 - - -Here is an example bash command using the Czechia provider: - -```bash -CZECHIA_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns czechia -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `CZECHIA_TOKEN` | Authorization token | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `CZECHIA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `CZECHIA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `CZECHIA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `CZECHIA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://api.czechia.com/swagger/index.html) - - - - diff --git a/docs/content/dns/zz_gen_ddnss.md b/docs/content/dns/zz_gen_ddnss.md deleted file mode 100644 index e159d58b4..000000000 --- a/docs/content/dns/zz_gen_ddnss.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: "DDnss (DynDNS Service)" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: ddnss -dnsprovider: - since: "v4.32.0" - code: "ddnss" - url: "https://ddnss.de/" ---- - - - - - - -Configuration for [DDnss (DynDNS Service)](https://ddnss.de/). - - - - -- Code: `ddnss` -- Since: v4.32.0 - - -Here is an example bash command using the DDnss (DynDNS Service) provider: - -```bash -DDNSS_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns ddnss -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `DDNSS_KEY` | Update key | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `DDNSS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `DDNSS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `DDNSS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `DDNSS_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) | -| `DDNSS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://ddnss.de/info.php) - - - - diff --git a/docs/content/dns/zz_gen_derak.md b/docs/content/dns/zz_gen_derak.md index c5c8c7bc6..f6605f6ab 100644 --- a/docs/content/dns/zz_gen_derak.md +++ b/docs/content/dns/zz_gen_derak.md @@ -27,7 +27,7 @@ Here is an example bash command using the Derak Cloud provider: ```bash DERAK_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns derak -d '*.example.com' -d example.com run +lego --email myemail@example.com --dns derak --domains my.example.org run ``` @@ -40,21 +40,21 @@ lego --dns derak -d '*.example.com' -d example.com run | `DERAK_API_KEY` | The API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `DERAK_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `DERAK_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 5) | -| `DERAK_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `DERAK_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `DERAK_HTTP_TIMEOUT` | API request timeout | +| `DERAK_POLLING_INTERVAL` | Time between DNS propagation check | +| `DERAK_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `DERAK_TTL` | The TTL of the TXT record used for the DNS challenge | | `DERAK_WEBSITE_ID` | Force the zone/website ID | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_desec.md b/docs/content/dns/zz_gen_desec.md index 4dbc713d6..b9faecc48 100644 --- a/docs/content/dns/zz_gen_desec.md +++ b/docs/content/dns/zz_gen_desec.md @@ -27,7 +27,7 @@ Here is an example bash command using the deSEC.io provider: ```bash DESEC_TOKEN=x-xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --dns desec -d '*.example.com' -d example.com run +lego --email you@example.com --dns desec --domains my.example.org run ``` @@ -40,20 +40,20 @@ lego --dns desec -d '*.example.com' -d example.com run | `DESEC_TOKEN` | Domain token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `DESEC_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `DESEC_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 4) | -| `DESEC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `DESEC_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) | +| `DESEC_HTTP_TIMEOUT` | API request timeout | +| `DESEC_POLLING_INTERVAL` | Time between DNS propagation check | +| `DESEC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `DESEC_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_designate.md b/docs/content/dns/zz_gen_designate.md index 9703f094d..10e24c83b 100644 --- a/docs/content/dns/zz_gen_designate.md +++ b/docs/content/dns/zz_gen_designate.md @@ -28,7 +28,7 @@ Here is an example bash command using the Designate DNSaaS for Openstack provide ```bash # With a `clouds.yaml` OS_CLOUD=my_openstack \ -lego --dns designate -d '*.example.com' -d example.com run +lego --email you@example.com --dns designate --domains my.example.org run # or @@ -37,7 +37,7 @@ OS_REGION_NAME=RegionOne \ OS_PROJECT_ID=23d4522a987d4ab529f722a007c27846 OS_USERNAME=myuser \ OS_PASSWORD=passw0rd \ -lego --dns designate -d '*.example.com' -d example.com run +lego --email you@example.com --dns designate --domains my.example.org run # or @@ -46,7 +46,7 @@ OS_REGION_NAME=RegionOne \ OS_AUTH_TYPE=v3applicationcredential \ OS_APPLICATION_CREDENTIAL_ID=imn74uq0or7dyzz20dwo1ytls4me8dry \ OS_APPLICATION_CREDENTIAL_SECRET=68FuSPSdQqkFQYH5X1OoriEIJOwyLtQ8QSqXZOc9XxFK1A9tzZT6He2PfPw0OMja \ -lego --dns designate -d '*.example.com' -d example.com run +lego --email you@example.com --dns designate --domains my.example.org run ``` @@ -67,22 +67,21 @@ lego --dns designate -d '*.example.com' -d example.com run | `OS_USER_ID` | User ID | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `DESIGNATE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | -| `DESIGNATE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 600) | -| `DESIGNATE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 10) | -| `DESIGNATE_ZONE_NAME` | The zone name to use in the OpenStack Project to manage TXT records. | +| `DESIGNATE_POLLING_INTERVAL` | Time between DNS propagation check | +| `DESIGNATE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `DESIGNATE_TTL` | The TTL of the TXT record used for the DNS challenge | | `OS_PROJECT_ID` | Project ID | | `OS_TENANT_NAME` | Tenant name (deprecated see OS_PROJECT_NAME and OS_PROJECT_ID) | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Description diff --git a/docs/content/dns/zz_gen_digitalocean.md b/docs/content/dns/zz_gen_digitalocean.md index 4dc43886d..8e86addeb 100644 --- a/docs/content/dns/zz_gen_digitalocean.md +++ b/docs/content/dns/zz_gen_digitalocean.md @@ -27,7 +27,7 @@ Here is an example bash command using the Digital Ocean provider: ```bash DO_AUTH_TOKEN=xxxxxx \ -lego --dns digitalocean -d '*.example.com' -d example.com run +lego --email you@example.com --dns digitalocean --domains my.example.org run ``` @@ -40,7 +40,7 @@ lego --dns digitalocean -d '*.example.com' -d example.com run | `DO_AUTH_TOKEN` | Authentication token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -48,13 +48,13 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | Environment Variable Name | Description | |--------------------------------|-------------| | `DO_API_URL` | The URL of the API | -| `DO_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `DO_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 5) | -| `DO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `DO_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 30) | +| `DO_HTTP_TIMEOUT` | API request timeout | +| `DO_POLLING_INTERVAL` | Time between DNS propagation check | +| `DO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `DO_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_directadmin.md b/docs/content/dns/zz_gen_directadmin.md deleted file mode 100644 index 1d03dcc4e..000000000 --- a/docs/content/dns/zz_gen_directadmin.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: "DirectAdmin" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: directadmin -dnsprovider: - since: "v4.18.0" - code: "directadmin" - url: "https://www.directadmin.com" ---- - - - - - - -Configuration for [DirectAdmin](https://www.directadmin.com). - - - - -- Code: `directadmin` -- Since: v4.18.0 - - -Here is an example bash command using the DirectAdmin provider: - -```bash -DIRECTADMIN_API_URL="http://example.com:2222" \ -DIRECTADMIN_USERNAME=xxxx \ -DIRECTADMIN_PASSWORD=yyy \ -lego --dns directadmin -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `DIRECTADMIN_API_URL` | URL of the API | -| `DIRECTADMIN_PASSWORD` | API password | -| `DIRECTADMIN_USERNAME` | API username | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `DIRECTADMIN_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `DIRECTADMIN_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 5) | -| `DIRECTADMIN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `DIRECTADMIN_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 30) | -| `DIRECTADMIN_ZONE_NAME` | Zone name used to add the TXT record | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://www.directadmin.com/api.php) - - - - diff --git a/docs/content/dns/zz_gen_dnsexit.md b/docs/content/dns/zz_gen_dnsexit.md deleted file mode 100644 index aca5357e8..000000000 --- a/docs/content/dns/zz_gen_dnsexit.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: "DNSExit" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: dnsexit -dnsprovider: - since: "v4.32.0" - code: "dnsexit" - url: "https://dnsexit.com" ---- - - - - - - -Configuration for [DNSExit](https://dnsexit.com). - - - - -- Code: `dnsexit` -- Since: v4.32.0 - - -Here is an example bash command using the DNSExit provider: - -```bash -DNSEXIT_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns dnsexit -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `DNSEXIT_API_KEY` | API key | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `DNSEXIT_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `DNSEXIT_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | -| `DNSEXIT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) | -| `DNSEXIT_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://dnsexit.com/dns/dns-api/) - - - - diff --git a/docs/content/dns/zz_gen_dnshomede.md b/docs/content/dns/zz_gen_dnshomede.md index ca7f83523..cbd475e5b 100644 --- a/docs/content/dns/zz_gen_dnshomede.md +++ b/docs/content/dns/zz_gen_dnshomede.md @@ -26,11 +26,11 @@ Configuration for [dnsHome.de](https://www.dnshome.de). Here is an example bash command using the dnsHome.de provider: ```bash -DNSHOMEDE_CREDENTIALS=example.org:password \ -lego --dns dnshomede -d '*.example.com' -d example.com run +DNSHOMEDE_CREDENTIALS=sub.example.org:password \ +lego --email you@example.com --dns dnshomede --domains example.org --domains '*.example.org' run DNSHOMEDE_CREDENTIALS=my.example.org:password1,demo.example.org:password2 \ -lego --dns dnshomede -d my.example.org -d demo.example.org +lego --email you@example.com --dns dnshomede --domains my.example.org --domains demo.example.org ``` @@ -43,20 +43,9 @@ lego --dns dnshomede -d my.example.org -d demo.example.org | `DNSHOMEDE_CREDENTIALS` | Comma-separated list of domain:password credential pairs | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `DNSHOMEDE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `DNSHOMEDE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 1200) | -| `DNSHOMEDE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 2) | -| `DNSHOMEDE_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). diff --git a/docs/content/dns/zz_gen_dnsimple.md b/docs/content/dns/zz_gen_dnsimple.md index 7799ece88..5a8b5bfc1 100644 --- a/docs/content/dns/zz_gen_dnsimple.md +++ b/docs/content/dns/zz_gen_dnsimple.md @@ -27,7 +27,7 @@ Here is an example bash command using the DNSimple provider: ```bash DNSIMPLE_OAUTH_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \ -lego --dns dnsimple -d '*.example.com' -d example.com run +lego --email you@example.com --dns dnsimple --domains my.example.org run ``` @@ -40,7 +40,7 @@ lego --dns dnsimple -d '*.example.com' -d example.com run | `DNSIMPLE_OAUTH_TOKEN` | OAuth token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -48,12 +48,12 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | Environment Variable Name | Description | |--------------------------------|-------------| | `DNSIMPLE_BASE_URL` | API endpoint URL | -| `DNSIMPLE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `DNSIMPLE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `DNSIMPLE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `DNSIMPLE_POLLING_INTERVAL` | Time between DNS propagation check | +| `DNSIMPLE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `DNSIMPLE_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Description diff --git a/docs/content/dns/zz_gen_dnsmadeeasy.md b/docs/content/dns/zz_gen_dnsmadeeasy.md index e7f260889..26a26fc42 100644 --- a/docs/content/dns/zz_gen_dnsmadeeasy.md +++ b/docs/content/dns/zz_gen_dnsmadeeasy.md @@ -28,7 +28,7 @@ Here is an example bash command using the DNS Made Easy provider: ```bash DNSMADEEASY_API_KEY=xxxxxx \ DNSMADEEASY_API_SECRET=yyyyy \ -lego --dns dnsmadeeasy -d '*.example.com' -d example.com run +lego --email you@example.com --dns dnsmadeeasy --domains my.example.org run ``` @@ -42,21 +42,21 @@ lego --dns dnsmadeeasy -d '*.example.com' -d example.com run | `DNSMADEEASY_API_SECRET` | The API Secret key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `DNSMADEEASY_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | -| `DNSMADEEASY_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `DNSMADEEASY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `DNSMADEEASY_HTTP_TIMEOUT` | API request timeout | +| `DNSMADEEASY_POLLING_INTERVAL` | Time between DNS propagation check | +| `DNSMADEEASY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `DNSMADEEASY_SANDBOX` | Activate the sandbox (boolean) | -| `DNSMADEEASY_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `DNSMADEEASY_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_dnspod.md b/docs/content/dns/zz_gen_dnspod.md index 86112a5ce..fc58ecec1 100644 --- a/docs/content/dns/zz_gen_dnspod.md +++ b/docs/content/dns/zz_gen_dnspod.md @@ -27,7 +27,7 @@ Here is an example bash command using the DNSPod (deprecated) provider: ```bash DNSPOD_API_KEY=xxxxxx \ -lego --dns dnspod -d '*.example.com' -d example.com run +lego --email you@example.com --dns dnspod --domains my.example.org run ``` @@ -40,20 +40,20 @@ lego --dns dnspod -d '*.example.com' -d example.com run | `DNSPOD_API_KEY` | The user token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `DNSPOD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `DNSPOD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `DNSPOD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `DNSPOD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) | +| `DNSPOD_HTTP_TIMEOUT` | API request timeout | +| `DNSPOD_POLLING_INTERVAL` | Time between DNS propagation check | +| `DNSPOD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `DNSPOD_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_dode.md b/docs/content/dns/zz_gen_dode.md index 28eebe5fa..6ea0c89ec 100644 --- a/docs/content/dns/zz_gen_dode.md +++ b/docs/content/dns/zz_gen_dode.md @@ -27,7 +27,7 @@ Here is an example bash command using the Domain Offensive (do.de) provider: ```bash DODE_TOKEN=xxxxxx \ -lego --dns dode -d '*.example.com' -d example.com run +lego --email you@example.com --dns dode --domains my.example.org run ``` @@ -40,20 +40,21 @@ lego --dns dode -d '*.example.com' -d example.com run | `DODE_TOKEN` | API token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `DODE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `DODE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `DODE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `DODE_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) | +| `DODE_HTTP_TIMEOUT` | API request timeout | +| `DODE_POLLING_INTERVAL` | Time between DNS propagation check | +| `DODE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `DODE_SEQUENCE_INTERVAL` | Time between sequential requests | +| `DODE_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_domeneshop.md b/docs/content/dns/zz_gen_domeneshop.md index 0530ab365..4a35b7b9e 100644 --- a/docs/content/dns/zz_gen_domeneshop.md +++ b/docs/content/dns/zz_gen_domeneshop.md @@ -28,7 +28,7 @@ Here is an example bash command using the Domeneshop provider: ```bash DOMENESHOP_API_TOKEN= \ DOMENESHOP_API_SECRET= \ -lego --dns domeneshop -d '*.example.com' -d example.com run +lego --email example@example.com --dns domeneshop --domains example.com run ``` @@ -42,19 +42,19 @@ lego --dns domeneshop -d '*.example.com' -d example.com run | `DOMENESHOP_API_TOKEN` | API token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `DOMENESHOP_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `DOMENESHOP_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 20) | -| `DOMENESHOP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) | +| `DOMENESHOP_HTTP_TIMEOUT` | API request timeout | +| `DOMENESHOP_POLLING_INTERVAL` | Time between DNS propagation check | +| `DOMENESHOP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ### API credentials diff --git a/docs/content/dns/zz_gen_dreamhost.md b/docs/content/dns/zz_gen_dreamhost.md index b9d273099..542a6f222 100644 --- a/docs/content/dns/zz_gen_dreamhost.md +++ b/docs/content/dns/zz_gen_dreamhost.md @@ -27,7 +27,7 @@ Here is an example bash command using the DreamHost provider: ```bash DREAMHOST_API_KEY="YOURAPIKEY" \ -lego --dns dreamhost -d '*.example.com' -d example.com run +lego --email you@example.com --dns dreamhost --domains my.example.org run ``` @@ -40,19 +40,20 @@ lego --dns dreamhost -d '*.example.com' -d example.com run | `DREAMHOST_API_KEY` | The API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `DREAMHOST_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `DREAMHOST_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 60) | -| `DREAMHOST_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 3600) | +| `DREAMHOST_HTTP_TIMEOUT` | API request timeout | +| `DREAMHOST_POLLING_INTERVAL` | Time between DNS propagation check | +| `DREAMHOST_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `DREAMHOST_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_duckdns.md b/docs/content/dns/zz_gen_duckdns.md index 8b60780d2..0851753cc 100644 --- a/docs/content/dns/zz_gen_duckdns.md +++ b/docs/content/dns/zz_gen_duckdns.md @@ -27,7 +27,7 @@ Here is an example bash command using the Duck DNS provider: ```bash DUCKDNS_TOKEN=xxxxxx \ -lego --dns duckdns -d '*.example.com' -d example.com run +lego --email you@example.com --dns duckdns --domains my.example.org run ``` @@ -40,20 +40,21 @@ lego --dns duckdns -d '*.example.com' -d example.com run | `DUCKDNS_TOKEN` | Account token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `DUCKDNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `DUCKDNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `DUCKDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `DUCKDNS_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) | +| `DUCKDNS_HTTP_TIMEOUT` | API request timeout | +| `DUCKDNS_POLLING_INTERVAL` | Time between DNS propagation check | +| `DUCKDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `DUCKDNS_SEQUENCE_INTERVAL` | Time between sequential requests | +| `DUCKDNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_dyn.md b/docs/content/dns/zz_gen_dyn.md index e31a90e45..53a91039f 100644 --- a/docs/content/dns/zz_gen_dyn.md +++ b/docs/content/dns/zz_gen_dyn.md @@ -29,7 +29,7 @@ Here is an example bash command using the Dyn provider: DYN_CUSTOMER_NAME=xxxxxx \ DYN_USER_NAME=yyyyy \ DYN_PASSWORD=zzzz \ -lego --dns dyn -d '*.example.com' -d example.com run +lego --email you@example.com --dns dyn --domains my.example.org run ``` @@ -44,20 +44,20 @@ lego --dns dyn -d '*.example.com' -d example.com run | `DYN_USER_NAME` | User name | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `DYN_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | -| `DYN_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `DYN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `DYN_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `DYN_HTTP_TIMEOUT` | API request timeout | +| `DYN_POLLING_INTERVAL` | Time between DNS propagation check | +| `DYN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `DYN_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_dyndnsfree.md b/docs/content/dns/zz_gen_dyndnsfree.md deleted file mode 100644 index ea549b4e2..000000000 --- a/docs/content/dns/zz_gen_dyndnsfree.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: "DynDnsFree.de" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: dyndnsfree -dnsprovider: - since: "v4.23.0" - code: "dyndnsfree" - url: "https://www.dyndnsfree.de" ---- - - - - - - -Configuration for [DynDnsFree.de](https://www.dyndnsfree.de). - - - - -- Code: `dyndnsfree` -- Since: v4.23.0 - - -Here is an example bash command using the DynDnsFree.de provider: - -```bash -DYNDNSFREE_USERNAME="xxx" \ -DYNDNSFREE_PASSWORD="yyy" \ -lego --dns dyndnsfree -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `DYNDNSFREE_PASSWORD` | Password | -| `DYNDNSFREE_USERNAME` | Username | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `DYNDNSFREE_HTTP_TIMEOUT` | Request timeout in seconds (Default: 30) | -| `DYNDNSFREE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `DYNDNSFREE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://www.dyndnsfree.de/user/hilfe.php?hsm=2) - - - - diff --git a/docs/content/dns/zz_gen_dynu.md b/docs/content/dns/zz_gen_dynu.md index a1f3e762e..58b2136cd 100644 --- a/docs/content/dns/zz_gen_dynu.md +++ b/docs/content/dns/zz_gen_dynu.md @@ -27,7 +27,7 @@ Here is an example bash command using the Dynu provider: ```bash DYNU_API_KEY=1234567890abcdefghijklmnopqrstuvwxyz \ -lego --dns dynu -d '*.example.com' -d example.com run +lego --email you@example.com --dns dynu --domains my.example.org run ``` @@ -40,20 +40,20 @@ lego --dns dynu -d '*.example.com' -d example.com run | `DYNU_API_KEY` | API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `DYNU_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `DYNU_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | -| `DYNU_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 180) | -| `DYNU_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `DYNU_HTTP_TIMEOUT` | API request timeout | +| `DYNU_POLLING_INTERVAL` | Time between DNS propagation check | +| `DYNU_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `DYNU_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_easydns.md b/docs/content/dns/zz_gen_easydns.md index 12f69e09c..e7b1f8ee1 100644 --- a/docs/content/dns/zz_gen_easydns.md +++ b/docs/content/dns/zz_gen_easydns.md @@ -26,9 +26,9 @@ Configuration for [EasyDNS](https://easydns.com/). Here is an example bash command using the EasyDNS provider: ```bash -EASYDNS_TOKEN=xxx \ -EASYDNS_KEY=yyy \ -lego --dns easydns -d '*.example.com' -d example.com run +EASYDNS_TOKEN= \ +EASYDNS_KEY= \ +lego --email you@example.com --dns easydns --domains my.example.org run ``` @@ -42,7 +42,7 @@ lego --dns easydns -d '*.example.com' -d example.com run | `EASYDNS_TOKEN` | API Token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -50,14 +50,14 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | Environment Variable Name | Description | |--------------------------------|-------------| | `EASYDNS_ENDPOINT` | The endpoint URL of the API Server | -| `EASYDNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `EASYDNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `EASYDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `EASYDNS_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) | -| `EASYDNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `EASYDNS_HTTP_TIMEOUT` | API request timeout | +| `EASYDNS_POLLING_INTERVAL` | Time between DNS propagation check | +| `EASYDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `EASYDNS_SEQUENCE_INTERVAL` | Time between sequential requests | +| `EASYDNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). To test with the sandbox environment set ```EASYDNS_ENDPOINT=https://sandbox.rest.easydns.net``` diff --git a/docs/content/dns/zz_gen_edgecenter.md b/docs/content/dns/zz_gen_edgecenter.md deleted file mode 100644 index 1fd9fe5fa..000000000 --- a/docs/content/dns/zz_gen_edgecenter.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: "EdgeCenter" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: edgecenter -dnsprovider: - since: "v4.29.0" - code: "edgecenter" - url: "https://edgecenter.ru/dns" ---- - - - - - - -Configuration for [EdgeCenter](https://edgecenter.ru/dns). - - - - -- Code: `edgecenter` -- Since: v4.29.0 - - -Here is an example bash command using the EdgeCenter provider: - -```bash -EDGECENTER_PERMANENT_API_TOKEN=xxxxx \ -lego --dns edgecenter -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `EDGECENTER_PERMANENT_API_TOKEN` | Permanent API token (https://edgecenter.ru/blog/permanent-api-token-explained/) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `EDGECENTER_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | -| `EDGECENTER_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 20) | -| `EDGECENTER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 360) | -| `EDGECENTER_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://apidocs.edgecenter.ru/dns) - - - - diff --git a/docs/content/dns/zz_gen_edgedns.md b/docs/content/dns/zz_gen_edgedns.md index 31b191168..b9af37ead 100644 --- a/docs/content/dns/zz_gen_edgedns.md +++ b/docs/content/dns/zz_gen_edgedns.md @@ -30,7 +30,7 @@ AKAMAI_CLIENT_SECRET=abcdefghijklmnopqrstuvwxyz1234567890ABCDEFG= \ AKAMAI_CLIENT_TOKEN=akab-mnbvcxzlkjhgfdsapoiuytrewq1234567 \ AKAMAI_HOST=akab-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.luna.akamaiapis.net \ AKAMAI_ACCESS_TOKEN=akab-1234567890qwerty-asdfghjklzxcvtnu \ -lego --dns edgedns -d '*.example.com' -d example.com run +lego --email you@example.com --dns edgedns --domains my.example.org run ``` @@ -48,20 +48,19 @@ lego --dns edgedns -d '*.example.com' -d example.com run | `AKAMAI_HOST` | API host, managed by the Akamai EdgeGrid client | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `AKAMAI_ACCOUNT_SWITCH_KEY` | Target account ID when the DNS zone and credentials belong to different accounts | -| `AKAMAI_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 15) | -| `AKAMAI_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 180) | -| `AKAMAI_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `AKAMAI_POLLING_INTERVAL` | Time between DNS propagation check. Default: 15 seconds | +| `AKAMAI_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation. Default: 3 minutes | +| `AKAMAI_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). Akamai's credentials are automatically detected in the following locations and prioritized in the following order: @@ -89,7 +88,6 @@ See also: - [.edgerc Format](https://developer.akamai.com/legacy/introduction/Conf_Client.html#edgercformat) - [API Client Authentication](https://developer.akamai.com/legacy/introduction/Client_Auth.html) - [Config from Env](https://github.com/akamai/AkamaiOPEN-edgegrid-golang/blob/master/pkg/edgegrid/config.go#L118) -- [Manage many accounts](https://techdocs.akamai.com/developer/docs/manage-many-accounts-with-one-api-client) diff --git a/docs/content/dns/zz_gen_edgeone.md b/docs/content/dns/zz_gen_edgeone.md deleted file mode 100644 index ba5de5ba2..000000000 --- a/docs/content/dns/zz_gen_edgeone.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -title: "Tencent EdgeOne" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: edgeone -dnsprovider: - since: "v4.26.0" - code: "edgeone" - url: "https://edgeone.ai" ---- - - - - - - -Configuration for [Tencent EdgeOne](https://edgeone.ai). - - - - -- Code: `edgeone` -- Since: v4.26.0 - - -Here is an example bash command using the Tencent EdgeOne provider: - -```bash -EDGEONE_SECRET_ID=abcdefghijklmnopqrstuvwx \ -EDGEONE_SECRET_KEY=your-secret-key \ -lego --dns edgeone -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `EDGEONE_SECRET_ID` | Access key ID | -| `EDGEONE_SECRET_KEY` | Access Key secret | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `EDGEONE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `EDGEONE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 30) | -| `EDGEONE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 1200) | -| `EDGEONE_REGION` | Region | -| `EDGEONE_SESSION_TOKEN` | Access Key token | -| `EDGEONE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | -| `EDGEONE_ZONES_MAPPING` | Mapping between DNS zones and site IDs. (ex: 'example.org:id1,example.com:id2') | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://edgeone.ai/document/50454#dns-record-apis) -- [Go client](https://github.com/tencentcloud/tencentcloud-sdk-go) - - - - diff --git a/docs/content/dns/zz_gen_efficientip.md b/docs/content/dns/zz_gen_efficientip.md index acca3ebb7..34f55f1d6 100644 --- a/docs/content/dns/zz_gen_efficientip.md +++ b/docs/content/dns/zz_gen_efficientip.md @@ -30,7 +30,7 @@ EFFICIENTIP_USERNAME="user" \ EFFICIENTIP_PASSWORD="secret" \ EFFICIENTIP_HOSTNAME="ipam.example.org" \ EFFICIENTIP_DNS_NAME="dns.smart" \ -lego --dns efficientip -d '*.example.com' -d example.com run +lego --email you@example.com --dns efficientip --domains my.example.org run ``` @@ -46,21 +46,22 @@ lego --dns efficientip -d '*.example.com' -d example.com run | `EFFICIENTIP_USERNAME` | Username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `EFFICIENTIP_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | +| `EFFICIENTIP_HTTP_TIMEOUT` | API request timeout | | `EFFICIENTIP_INSECURE_SKIP_VERIFY` | Whether or not to verify EfficientIP API certificate | -| `EFFICIENTIP_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `EFFICIENTIP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `EFFICIENTIP_POLLING_INTERVAL` | Time between DNS propagation check | +| `EFFICIENTIP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `EFFICIENTIP_TTL` | The TTL of the TXT record used for the DNS challenge | | `EFFICIENTIP_VIEW_NAME` | View name (ex: external) | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_epik.md b/docs/content/dns/zz_gen_epik.md index a7fc029d3..8f946fc3c 100644 --- a/docs/content/dns/zz_gen_epik.md +++ b/docs/content/dns/zz_gen_epik.md @@ -27,7 +27,7 @@ Here is an example bash command using the Epik provider: ```bash EPIK_SIGNATURE=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --dns epik -d '*.example.com' -d example.com run +lego --email you@example.com --dns epik --domains my.example.org run ``` @@ -40,27 +40,27 @@ lego --dns epik -d '*.example.com' -d example.com run | `EPIK_SIGNATURE` | Epik API signature (https://registrar.epik.com/account/api-settings/) | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `EPIK_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `EPIK_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `EPIK_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `EPIK_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) | +| `EPIK_HTTP_TIMEOUT` | API request timeout | +| `EPIK_POLLING_INTERVAL` | Time between DNS propagation check | +| `EPIK_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `EPIK_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information -- [API documentation](https://docs-userapi.epik.com/v2/) +- [API documentation](https://docs.userapi.epik.com/v2/#/) diff --git a/docs/content/dns/zz_gen_eurodns.md b/docs/content/dns/zz_gen_eurodns.md deleted file mode 100644 index cb5a0418d..000000000 --- a/docs/content/dns/zz_gen_eurodns.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: "EuroDNS" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: eurodns -dnsprovider: - since: "v4.33.0" - code: "eurodns" - url: "https://www.eurodns.com/" ---- - - - - - - -Configuration for [EuroDNS](https://www.eurodns.com/). - - - - -- Code: `eurodns` -- Since: v4.33.0 - - -Here is an example bash command using the EuroDNS provider: - -```bash -EURODNS_APP_ID="xxx" \ -EURODNS_API_KEY="yyy" \ -lego --dns eurodns -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `EURODNS_API_KEY` | API key | -| `EURODNS_APP_ID` | Application ID | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `EURODNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `EURODNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `EURODNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `EURODNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://docapi.eurodns.com/) - - - - diff --git a/docs/content/dns/zz_gen_excedo.md b/docs/content/dns/zz_gen_excedo.md deleted file mode 100644 index 456e6f60a..000000000 --- a/docs/content/dns/zz_gen_excedo.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: "Excedo" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: excedo -dnsprovider: - since: "v4.33.0" - code: "excedo" - url: "https://excedo.se/" ---- - - - - - - -Configuration for [Excedo](https://excedo.se/). - - - - -- Code: `excedo` -- Since: v4.33.0 - - -Here is an example bash command using the Excedo provider: - -```bash -EXCEDO_API_KEY=your-api-key \ -EXCEDO_API_URL=your-base-url \ -lego --dns excedo -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `EXCEDO_API_KEY` | API key | -| `EXCEDO_API_URL` | API base URL | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `EXCEDO_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `EXCEDO_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | -| `EXCEDO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) | -| `EXCEDO_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](none) - - - - diff --git a/docs/content/dns/zz_gen_exec.md b/docs/content/dns/zz_gen_exec.md index ad2e6906e..1d44bfcea 100644 --- a/docs/content/dns/zz_gen_exec.md +++ b/docs/content/dns/zz_gen_exec.md @@ -26,7 +26,7 @@ Here is an example bash command using the External program provider: ```bash EXEC_PATH=/the/path/to/myscript.sh \ -lego --dns exec -d '*.example.com' -d example.com run +lego --email you@example.com --dns exec --domains my.example.org run ``` @@ -43,11 +43,11 @@ lego --dns exec -d '*.example.com' -d example.com run ## Additional Configuration -| Environment Variable Name | Description | -|----------------------------|--------------------------------------------------------------------| -| `EXEC_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 3). | -| `EXEC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60). | -| `EXEC_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60). | +| Environment Variable Name | Description | +|----------------------------|-------------------------------------------| +| `EXEC_POLLING_INTERVAL` | Time between DNS propagation check. | +| `EXEC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation. | +| `EXEC_SEQUENCE_INTERVAL` | Time between sequential requests. | ## Description @@ -61,7 +61,9 @@ For example, requesting a certificate for the domain 'my.example.org' can be ach ```bash EXEC_PATH=./update-dns.sh \ -lego --dns exec --d my.example.org run + lego --email you@example.com \ + --dns exec \ + --domains my.example.org run ``` It will then call the program './update-dns.sh' with like this: @@ -81,7 +83,9 @@ If you want to use the raw domain, token, and keyAuth values with your program, ```bash EXEC_MODE=RAW \ EXEC_PATH=./update-dns.sh \ -lego --dns exec -d my.example.org run + lego --email you@example.com \ + --dns exec \ + --domains my.example.org run ``` It will then call the program `./update-dns.sh` like this: diff --git a/docs/content/dns/zz_gen_exoscale.md b/docs/content/dns/zz_gen_exoscale.md index e599d6487..48c7dec79 100644 --- a/docs/content/dns/zz_gen_exoscale.md +++ b/docs/content/dns/zz_gen_exoscale.md @@ -28,7 +28,7 @@ Here is an example bash command using the Exoscale provider: ```bash EXOSCALE_API_KEY=abcdefghijklmnopqrstuvwx \ EXOSCALE_API_SECRET=xxxxxxx \ -lego --dns exoscale -d '*.example.com' -d example.com run +lego --email you@example.com --dns exoscale --domains my.example.org run ``` @@ -42,21 +42,22 @@ lego --dns exoscale -d '*.example.com' -d example.com run | `EXOSCALE_API_SECRET` | API secret | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| +| `EXOSCALE_API_ZONE` | API zone | | `EXOSCALE_ENDPOINT` | API endpoint URL | -| `EXOSCALE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 60) | -| `EXOSCALE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `EXOSCALE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `EXOSCALE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `EXOSCALE_HTTP_TIMEOUT` | API request timeout | +| `EXOSCALE_POLLING_INTERVAL` | Time between DNS propagation check | +| `EXOSCALE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `EXOSCALE_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_f5xc.md b/docs/content/dns/zz_gen_f5xc.md deleted file mode 100644 index 0fd8fe58a..000000000 --- a/docs/content/dns/zz_gen_f5xc.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: "F5 XC" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: f5xc -dnsprovider: - since: "v4.23.0" - code: "f5xc" - url: "https://www.f5.com/products/distributed-cloud-services" ---- - - - - - - -Configuration for [F5 XC](https://www.f5.com/products/distributed-cloud-services). - - - - -- Code: `f5xc` -- Since: v4.23.0 - - -Here is an example bash command using the F5 XC provider: - -```bash -F5XC_API_TOKEN="xxx" \ -F5XC_TENANT_NAME="yyy" \ -F5XC_GROUP_NAME="zzz" \ -lego --dns f5xc -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `F5XC_API_TOKEN` | API token | -| `F5XC_GROUP_NAME` | Group name | -| `F5XC_TENANT_NAME` | XC Tenant shortname | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `F5XC_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `F5XC_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `F5XC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `F5XC_SERVER` | Server domain (Default: console.ves.volterra.io) | -| `F5XC_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://docs.cloud.f5.com/docs-v2/api/dns-zone-rrset) - - - - diff --git a/docs/content/dns/zz_gen_freemyip.md b/docs/content/dns/zz_gen_freemyip.md index 215f8eb84..929f62d5c 100644 --- a/docs/content/dns/zz_gen_freemyip.md +++ b/docs/content/dns/zz_gen_freemyip.md @@ -27,7 +27,7 @@ Here is an example bash command using the freemyip.com provider: ```bash FREEMYIP_TOKEN=xxxxxx \ -lego --dns freemyip -d '*.example.com' -d example.com run +lego --email you@example.com --dns freemyip --domains my.example.org run ``` @@ -40,21 +40,21 @@ lego --dns freemyip -d '*.example.com' -d example.com run | `FREEMYIP_TOKEN` | Account token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `FREEMYIP_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `FREEMYIP_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `FREEMYIP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `FREEMYIP_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) | -| `FREEMYIP_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) | +| `FREEMYIP_HTTP_TIMEOUT` | API request timeout | +| `FREEMYIP_POLLING_INTERVAL` | Time between DNS propagation check | +| `FREEMYIP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `FREEMYIP_SEQUENCE_INTERVAL` | Time between sequential requests | +| `FREEMYIP_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_gandi.md b/docs/content/dns/zz_gen_gandi.md index b02d97819..703213616 100644 --- a/docs/content/dns/zz_gen_gandi.md +++ b/docs/content/dns/zz_gen_gandi.md @@ -27,7 +27,7 @@ Here is an example bash command using the Gandi provider: ```bash GANDI_API_KEY=abcdefghijklmnopqrstuvwx \ -lego --dns gandi -d '*.example.com' -d example.com run +lego --email you@example.com --dns gandi --domains my.example.org run ``` @@ -40,20 +40,20 @@ lego --dns gandi -d '*.example.com' -d example.com run | `GANDI_API_KEY` | API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `GANDI_HTTP_TIMEOUT` | API request timeout in seconds (Default: 60) | -| `GANDI_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 60) | -| `GANDI_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 2400) | -| `GANDI_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `GANDI_HTTP_TIMEOUT` | API request timeout | +| `GANDI_POLLING_INTERVAL` | Time between DNS propagation check | +| `GANDI_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `GANDI_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_gandiv5.md b/docs/content/dns/zz_gen_gandiv5.md index 78824abbe..d88cc25a0 100644 --- a/docs/content/dns/zz_gen_gandiv5.md +++ b/docs/content/dns/zz_gen_gandiv5.md @@ -27,7 +27,7 @@ Here is an example bash command using the Gandi Live DNS (v5) provider: ```bash GANDIV5_PERSONAL_ACCESS_TOKEN=abcdefghijklmnopqrstuvwx \ -lego --dns gandiv5 -d '*.example.com' -d example.com run +lego --email you@example.com --dns gandiv5 --domains my.example.org run ``` @@ -41,20 +41,20 @@ lego --dns gandiv5 -d '*.example.com' -d example.com run | `GANDIV5_PERSONAL_ACCESS_TOKEN` | Personal Access Token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `GANDIV5_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | -| `GANDIV5_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 20) | -| `GANDIV5_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 1200) | -| `GANDIV5_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `GANDIV5_HTTP_TIMEOUT` | API request timeout | +| `GANDIV5_POLLING_INTERVAL` | Time between DNS propagation check | +| `GANDIV5_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `GANDIV5_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_gcloud.md b/docs/content/dns/zz_gen_gcloud.md index 64acc1d1e..714945282 100644 --- a/docs/content/dns/zz_gen_gcloud.md +++ b/docs/content/dns/zz_gen_gcloud.md @@ -26,21 +26,12 @@ Configuration for [Google Cloud](https://cloud.google.com). Here is an example bash command using the Google Cloud provider: ```bash -# Using a service account file -GCE_PROJECT="gc-project-id" \ -GCE_SERVICE_ACCOUNT_FILE="/path/to/svc/account/file.json" \ -lego --dns gcloud -d '*.example.com' -d example.com run - -# Using default credentials with impersonation -GCE_PROJECT="gc-project-id" \ -GCE_IMPERSONATE_SERVICE_ACCOUNT="target-sa@gc-project-id.iam.gserviceaccount.com" \ -lego --dns gcloud -d '*.example.com' -d example.com run - -# Using service account key with impersonation -GCE_PROJECT="gc-project-id" \ -GCE_SERVICE_ACCOUNT_FILE="/path/to/svc/account/file.json" \ -GCE_IMPERSONATE_SERVICE_ACCOUNT="target-sa@gc-project-id.iam.gserviceaccount.com" \ -lego --dns gcloud -d '*.example.com' -d example.com run +GCE_PROJECT="gc-project-id" GCE_SERVICE_ACCOUNT_FILE="/path/to/svc/account/file.json" lego \ + --email="abc@email.com" \ + --domains="example.com" \ + --dns="gcloud" \ + --path="${HOME}/.lego" \ + run ``` @@ -56,7 +47,7 @@ lego --dns gcloud -d '*.example.com' -d example.com run | `GCE_SERVICE_ACCOUNT_FILE` | Account file path | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -64,20 +55,14 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | Environment Variable Name | Description | |--------------------------------|-------------| | `GCE_ALLOW_PRIVATE_ZONE` | Allows requested domain to be in private DNS zone, works only with a private ACME server (by default: false) | -| `GCE_IMPERSONATE_SERVICE_ACCOUNT` | Service account email to impersonate | -| `GCE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 5) | -| `GCE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 180) | -| `GCE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `GCE_POLLING_INTERVAL` | Time between DNS propagation check | +| `GCE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `GCE_TTL` | The TTL of the TXT record used for the DNS challenge | | `GCE_ZONE_ID` | Allows to skip the automatic detection of the zone | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). -Supports service account impersonation to access Google Cloud DNS resources across different projects or with restricted permissions. - -When using impersonation, the source service account must have: -1. The "Service Account Token Creator" role on the source service account -2. The "https://www.googleapis.com/auth/cloud-platform" scope diff --git a/docs/content/dns/zz_gen_gcore.md b/docs/content/dns/zz_gen_gcore.md index 21a7ee9b1..731a7095b 100644 --- a/docs/content/dns/zz_gen_gcore.md +++ b/docs/content/dns/zz_gen_gcore.md @@ -27,7 +27,7 @@ Here is an example bash command using the G-Core provider: ```bash GCORE_PERMANENT_API_TOKEN=xxxxx \ -lego --dns gcore -d '*.example.com' -d example.com run +lego --email you@example.com --dns gcore --domains my.example.org run ``` @@ -40,20 +40,20 @@ lego --dns gcore -d '*.example.com' -d example.com run | `GCORE_PERMANENT_API_TOKEN` | Permanent API token (https://gcore.com/blog/permanent-api-token-explained/) | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `GCORE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | -| `GCORE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 20) | -| `GCORE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 360) | -| `GCORE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `GCORE_HTTP_TIMEOUT` | API request timeout | +| `GCORE_POLLING_INTERVAL` | Time between DNS propagation check | +| `GCORE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `GCORE_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_gigahostno.md b/docs/content/dns/zz_gen_gigahostno.md deleted file mode 100644 index a59ffc401..000000000 --- a/docs/content/dns/zz_gen_gigahostno.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: "Gigahost.no" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: gigahostno -dnsprovider: - since: "v4.29.0" - code: "gigahostno" - url: "https://gigahost.no/" ---- - - - - - - -Configuration for [Gigahost.no](https://gigahost.no/). - - - - -- Code: `gigahostno` -- Since: v4.29.0 - - -Here is an example bash command using the Gigahost.no provider: - -```bash -GIGAHOSTNO_USERNAME="xxxxxxxxxxxxxxxxxxxxx" \ -GIGAHOSTNO_PASSWORD="yyyyyyyyyyyyyyyyyyyyy" \ -lego --dns gigahostno -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `GIGAHOSTNO_PASSWORD` | Password | -| `GIGAHOSTNO_USERNAME` | Username | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `GIGAHOSTNO_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `GIGAHOSTNO_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `GIGAHOSTNO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `GIGAHOSTNO_SECRET` | TOTP secret | -| `GIGAHOSTNO_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://gigahost.no/api-dokumentasjon) - - - - diff --git a/docs/content/dns/zz_gen_glesys.md b/docs/content/dns/zz_gen_glesys.md index 2d2608330..1e8322d66 100644 --- a/docs/content/dns/zz_gen_glesys.md +++ b/docs/content/dns/zz_gen_glesys.md @@ -28,7 +28,7 @@ Here is an example bash command using the Glesys provider: ```bash GLESYS_API_USER=xxxxx \ GLESYS_API_KEY=yyyyy \ -lego --dns glesys -d '*.example.com' -d example.com run +lego --email you@example.com --dns glesys --domains my.example.org run ``` @@ -42,20 +42,20 @@ lego --dns glesys -d '*.example.com' -d example.com run | `GLESYS_API_USER` | API user | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `GLESYS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | -| `GLESYS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 20) | -| `GLESYS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 1200) | -| `GLESYS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | +| `GLESYS_HTTP_TIMEOUT` | API request timeout | +| `GLESYS_POLLING_INTERVAL` | Time between DNS propagation check | +| `GLESYS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `GLESYS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_godaddy.md b/docs/content/dns/zz_gen_godaddy.md index bc51cd69b..a09923ac6 100644 --- a/docs/content/dns/zz_gen_godaddy.md +++ b/docs/content/dns/zz_gen_godaddy.md @@ -28,7 +28,7 @@ Here is an example bash command using the Go Daddy provider: ```bash GODADDY_API_KEY=xxxxxxxx \ GODADDY_API_SECRET=yyyyyyyy \ -lego --dns godaddy -d '*.example.com' -d example.com run +lego --email you@example.com --dns godaddy --domains my.example.org run ``` @@ -42,20 +42,20 @@ lego --dns godaddy -d '*.example.com' -d example.com run | `GODADDY_API_SECRET` | API secret | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `GODADDY_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `GODADDY_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `GODADDY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `GODADDY_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) | +| `GODADDY_HTTP_TIMEOUT` | API request timeout | +| `GODADDY_POLLING_INTERVAL` | Time between DNS propagation check | +| `GODADDY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `GODADDY_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). GoDaddy has recently (2024-04) updated the account requirements to access parts of their production Domains API: diff --git a/docs/content/dns/zz_gen_googledomains.md b/docs/content/dns/zz_gen_googledomains.md index 2421184c0..9ba257987 100644 --- a/docs/content/dns/zz_gen_googledomains.md +++ b/docs/content/dns/zz_gen_googledomains.md @@ -6,15 +6,15 @@ slug: googledomains dnsprovider: since: "v4.11.0" code: "googledomains" - url: "https://github.com/go-acme/lego/issues/2553" + url: "https://domains.google" --- -The Google Domains DNS provider has shut down. +Configuration for [Google Domains](https://domains.google). @@ -27,7 +27,7 @@ Here is an example bash command using the Google Domains provider: ```bash GOOGLE_DOMAINS_ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --dns googledomains -d '*.example.com' -d example.com run +lego --email you@example.com --dns googledomains --domains my.example.org run ``` @@ -40,19 +40,19 @@ lego --dns googledomains -d '*.example.com' -d example.com run | `GOOGLE_DOMAINS_ACCESS_TOKEN` | Access token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `GOOGLE_DOMAINS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `GOOGLE_DOMAINS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `GOOGLE_DOMAINS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | +| `GOOGLE_DOMAINS_HTTP_TIMEOUT` | API request timeout | +| `GOOGLE_DOMAINS_POLLING_INTERVAL` | Time between DNS propagation check | +| `GOOGLE_DOMAINS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_gravity.md b/docs/content/dns/zz_gen_gravity.md deleted file mode 100644 index 654ad8424..000000000 --- a/docs/content/dns/zz_gen_gravity.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: "Gravity" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: gravity -dnsprovider: - since: "v4.30.0" - code: "gravity" - url: "https://gravity.beryju.io/" ---- - - - - - - -Configuration for [Gravity](https://gravity.beryju.io/). - - - - -- Code: `gravity` -- Since: v4.30.0 - - -Here is an example bash command using the Gravity provider: - -```bash -GRAVITY_SERVER_URL="https://example.org:1234" \ -GRAVITY_USERNAME="xxxxxxxxxxxxxxxxxxxxx" \ -GRAVITY_PASSWORD="yyyyyyyyyyyyyyyyyyyyy" \ -lego --dns gravity -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `GRAVITY_PASSWORD` | Password | -| `GRAVITY_SERVER_URL` | URL of the server | -| `GRAVITY_USERNAME` | Username | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `GRAVITY_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `GRAVITY_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `GRAVITY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `GRAVITY_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 1) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://gravity.beryju.io/docs/api/reference/) - - - - diff --git a/docs/content/dns/zz_gen_hetzner.md b/docs/content/dns/zz_gen_hetzner.md index 4e81bd4d9..38b5e888d 100644 --- a/docs/content/dns/zz_gen_hetzner.md +++ b/docs/content/dns/zz_gen_hetzner.md @@ -26,8 +26,8 @@ Configuration for [Hetzner](https://hetzner.com). Here is an example bash command using the Hetzner provider: ```bash -HETZNER_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns hetzner -d '*.example.com' -d example.com run +HETZNER_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ +lego --email you@example.com --dns hetzner --domains my.example.org run ``` @@ -37,30 +37,30 @@ lego --dns hetzner -d '*.example.com' -d example.com run | Environment Variable Name | Description | |-----------------------|-------------| -| `HETZNER_API_TOKEN` | API token | +| `HETZNER_API_KEY` | API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `HETZNER_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `HETZNER_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `HETZNER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `HETZNER_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `HETZNER_HTTP_TIMEOUT` | API request timeout | +| `HETZNER_POLLING_INTERVAL` | Time between DNS propagation check | +| `HETZNER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `HETZNER_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information -- [API documentation](https://docs.hetzner.cloud/reference/cloud#dns) +- [API documentation](https://dns.hetzner.com/api-docs) diff --git a/docs/content/dns/zz_gen_hostingde.md b/docs/content/dns/zz_gen_hostingde.md index 4a66fe0f1..a2986d3ab 100644 --- a/docs/content/dns/zz_gen_hostingde.md +++ b/docs/content/dns/zz_gen_hostingde.md @@ -27,7 +27,7 @@ Here is an example bash command using the Hosting.de provider: ```bash HOSTINGDE_API_KEY=xxxxxxxx \ -lego --dns hostingde -d '*.example.com' -d example.com run +lego --email you@example.com --dns hostingde --domains my.example.org run ``` @@ -40,21 +40,21 @@ lego --dns hostingde -d '*.example.com' -d example.com run | `HOSTINGDE_API_KEY` | API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `HOSTINGDE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `HOSTINGDE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `HOSTINGDE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `HOSTINGDE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `HOSTINGDE_HTTP_TIMEOUT` | API request timeout | +| `HOSTINGDE_POLLING_INTERVAL` | Time between DNS propagation check | +| `HOSTINGDE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `HOSTINGDE_TTL` | The TTL of the TXT record used for the DNS challenge | | `HOSTINGDE_ZONE_NAME` | Zone name in ACE format | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_hostinger.md b/docs/content/dns/zz_gen_hostinger.md deleted file mode 100644 index c05b3f003..000000000 --- a/docs/content/dns/zz_gen_hostinger.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: "Hostinger" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: hostinger -dnsprovider: - since: "v4.27.0" - code: "hostinger" - url: "https://www.hostinger.com/" ---- - - - - - - -Configuration for [Hostinger](https://www.hostinger.com/). - - - - -- Code: `hostinger` -- Since: v4.27.0 - - -Here is an example bash command using the Hostinger provider: - -```bash -HOSTINGER_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns hostinger -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `HOSTINGER_API_TOKEN` | API Token | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `HOSTINGER_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `HOSTINGER_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `HOSTINGER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `HOSTINGER_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://developers.hostinger.com/#tag/dns-zone) - - - - diff --git a/docs/content/dns/zz_gen_hostingnl.md b/docs/content/dns/zz_gen_hostingnl.md deleted file mode 100644 index 09cb69b47..000000000 --- a/docs/content/dns/zz_gen_hostingnl.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: "Hosting.nl" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: hostingnl -dnsprovider: - since: "v4.30.0" - code: "hostingnl" - url: "https://hosting.nl" ---- - - - - - - -Configuration for [Hosting.nl](https://hosting.nl). - - - - -- Code: `hostingnl` -- Since: v4.30.0 - - -Here is an example bash command using the Hosting.nl provider: - -```bash -HOSTINGNL_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns hostingnl -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `HOSTINGNL_API_KEY` | The API key | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `HOSTINGNL_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | -| `HOSTINGNL_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `HOSTINGNL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `HOSTINGNL_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://api.hosting.nl/api/documentation) - - - - diff --git a/docs/content/dns/zz_gen_hosttech.md b/docs/content/dns/zz_gen_hosttech.md index 9435cc562..c77165b04 100644 --- a/docs/content/dns/zz_gen_hosttech.md +++ b/docs/content/dns/zz_gen_hosttech.md @@ -27,7 +27,7 @@ Here is an example bash command using the Hosttech provider: ```bash HOSTTECH_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --dns hosttech -d '*.example.com' -d example.com run +lego --email you@example.com --dns hosttech --domains my.example.org run ``` @@ -41,20 +41,20 @@ lego --dns hosttech -d '*.example.com' -d example.com run | `HOSTTECH_PASSWORD` | API password | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `HOSTTECH_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `HOSTTECH_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `HOSTTECH_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `HOSTTECH_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) | +| `HOSTTECH_HTTP_TIMEOUT` | API request timeout | +| `HOSTTECH_POLLING_INTERVAL` | Time between DNS propagation check | +| `HOSTTECH_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `HOSTTECH_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_httpnet.md b/docs/content/dns/zz_gen_httpnet.md index 862909697..487718f2c 100644 --- a/docs/content/dns/zz_gen_httpnet.md +++ b/docs/content/dns/zz_gen_httpnet.md @@ -27,7 +27,7 @@ Here is an example bash command using the http.net provider: ```bash HTTPNET_API_KEY=xxxxxxxx \ -lego --dns httpnet -d '*.example.com' -d example.com run +lego --email you@example.com --dns httpnet --domains my.example.org run ``` @@ -40,21 +40,21 @@ lego --dns httpnet -d '*.example.com' -d example.com run | `HTTPNET_API_KEY` | API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `HTTPNET_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `HTTPNET_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `HTTPNET_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `HTTPNET_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `HTTPNET_HTTP_TIMEOUT` | API request timeout | +| `HTTPNET_POLLING_INTERVAL` | Time between DNS propagation check | +| `HTTPNET_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `HTTPNET_TTL` | The TTL of the TXT record used for the DNS challenge | | `HTTPNET_ZONE_NAME` | Zone name in ACE format | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_httpreq.md b/docs/content/dns/zz_gen_httpreq.md index 7f6a8d576..f54e61ece 100644 --- a/docs/content/dns/zz_gen_httpreq.md +++ b/docs/content/dns/zz_gen_httpreq.md @@ -27,7 +27,7 @@ Here is an example bash command using the HTTP request provider: ```bash HTTPREQ_ENDPOINT=http://my.server.com:9090 \ -lego --dns httpreq -d '*.example.com' -d example.com run +lego --email you@example.com --dns httpreq --domains my.example.org run ``` @@ -41,21 +41,21 @@ lego --dns httpreq -d '*.example.com' -d example.com run | `HTTPREQ_MODE` | `RAW`, none | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `HTTPREQ_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | +| `HTTPREQ_HTTP_TIMEOUT` | API request timeout | | `HTTPREQ_PASSWORD` | Basic authentication password | -| `HTTPREQ_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `HTTPREQ_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `HTTPREQ_POLLING_INTERVAL` | Time between DNS propagation check | +| `HTTPREQ_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `HTTPREQ_USERNAME` | Basic authentication username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Description diff --git a/docs/content/dns/zz_gen_huaweicloud.md b/docs/content/dns/zz_gen_huaweicloud.md deleted file mode 100644 index 46d121265..000000000 --- a/docs/content/dns/zz_gen_huaweicloud.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: "Huawei Cloud" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: huaweicloud -dnsprovider: - since: "v4.19" - code: "huaweicloud" - url: "https://huaweicloud.com" ---- - - - - - - -Configuration for [Huawei Cloud](https://huaweicloud.com). - - - - -- Code: `huaweicloud` -- Since: v4.19 - - -Here is an example bash command using the Huawei Cloud provider: - -```bash -HUAWEICLOUD_ACCESS_KEY_ID=your-access-key-id \ -HUAWEICLOUD_SECRET_ACCESS_KEY=your-secret-access-key \ -HUAWEICLOUD_REGION=cn-south-1 \ -lego --dns huaweicloud -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `HUAWEICLOUD_ACCESS_KEY_ID` | Access key ID | -| `HUAWEICLOUD_REGION` | Region | -| `HUAWEICLOUD_SECRET_ACCESS_KEY` | Access Key secret | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `HUAWEICLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `HUAWEICLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `HUAWEICLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `HUAWEICLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://console-intl.huaweicloud.com/apiexplorer/#/openapi/DNS/doc?locale=en-us) -- [Go client](https://github.com/huaweicloud/huaweicloud-sdk-go-v3) - - - - diff --git a/docs/content/dns/zz_gen_hurricane.md b/docs/content/dns/zz_gen_hurricane.md index 0c195d19c..0a2544690 100644 --- a/docs/content/dns/zz_gen_hurricane.md +++ b/docs/content/dns/zz_gen_hurricane.md @@ -27,10 +27,10 @@ Here is an example bash command using the Hurricane Electric DNS provider: ```bash HURRICANE_TOKENS=example.org:token \ -lego --dns hurricane -d '*.example.com' -d example.com run +lego --email you@example.com --dns hurricane --domains example.org --domains '*.example.org' run HURRICANE_TOKENS=my.example.org:token1,demo.example.org:token2 \ -lego --dns hurricane -d my.example.org -d demo.example.org +lego --email you@example.com --dns hurricane --domains my.example.org --domains demo.example.org ``` @@ -43,20 +43,9 @@ lego --dns hurricane -d my.example.org -d demo.example.org | `HURRICANE_TOKENS` | TXT record names and tokens | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `HURRICANE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `HURRICANE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `HURRICANE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation (Default: 300) | -| `HURRICANE_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). Before using lego to request a certificate for a given domain or wildcard (such as `my.example.org` or `*.my.example.org`), create a TXT record named `_acme-challenge.my.example.org`, and enable dynamic updates on it. diff --git a/docs/content/dns/zz_gen_hyperone.md b/docs/content/dns/zz_gen_hyperone.md index bc496f7bc..f976bf4ed 100644 --- a/docs/content/dns/zz_gen_hyperone.md +++ b/docs/content/dns/zz_gen_hyperone.md @@ -26,7 +26,7 @@ Configuration for [HyperOne](https://www.hyperone.com). Here is an example bash command using the HyperOne provider: ```bash -lego --dns hyperone -d '*.example.com' -d example.com run +lego --email you@example.com --dns hyperone --domains my.example.org run ``` @@ -39,15 +39,14 @@ lego --dns hyperone -d '*.example.com' -d example.com run | Environment Variable Name | Description | |--------------------------------|-------------| | `HYPERONE_API_URL` | Allows to pass custom API Endpoint to be used in the challenge (default https://api.hyperone.com/v2) | -| `HYPERONE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | | `HYPERONE_LOCATION_ID` | Specifies location (region) to be used in API calls. (default pl-waw-1) | | `HYPERONE_PASSPORT_LOCATION` | Allows to pass custom passport file location (default ~/.h1/passport.json) | -| `HYPERONE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 60) | -| `HYPERONE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 2) | -| `HYPERONE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `HYPERONE_POLLING_INTERVAL` | Time between DNS propagation check | +| `HYPERONE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `HYPERONE_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Description diff --git a/docs/content/dns/zz_gen_ibmcloud.md b/docs/content/dns/zz_gen_ibmcloud.md index c5a48d2ad..db9a6943a 100644 --- a/docs/content/dns/zz_gen_ibmcloud.md +++ b/docs/content/dns/zz_gen_ibmcloud.md @@ -28,7 +28,7 @@ Here is an example bash command using the IBM Cloud (SoftLayer) provider: ```bash SOFTLAYER_USERNAME=xxxxx \ SOFTLAYER_API_KEY=yyyyy \ -lego --dns ibmcloud -d '*.example.com' -d example.com run +lego --email you@example.com --dns ibmcloud --domains my.example.org run ``` @@ -39,23 +39,23 @@ lego --dns ibmcloud -d '*.example.com' -d example.com run | Environment Variable Name | Description | |-----------------------|-------------| | `SOFTLAYER_API_KEY` | Classic Infrastructure API key | -| `SOFTLAYER_USERNAME` | Username (IBM Cloud is {accountID}_{emailAddress}) | +| `SOFTLAYER_USERNAME` | Username (IBM Cloud is _) | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `SOFTLAYER_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `SOFTLAYER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `SOFTLAYER_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `SOFTLAYER_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `SOFTLAYER_POLLING_INTERVAL` | Time between DNS propagation check | +| `SOFTLAYER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `SOFTLAYER_TIMEOUT` | API request timeout | +| `SOFTLAYER_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_iij.md b/docs/content/dns/zz_gen_iij.md index c7acfe3a0..62df83066 100644 --- a/docs/content/dns/zz_gen_iij.md +++ b/docs/content/dns/zz_gen_iij.md @@ -29,7 +29,7 @@ Here is an example bash command using the Internet Initiative Japan provider: IIJ_API_ACCESS_KEY=xxxxxxxx \ IIJ_API_SECRET_KEY=yyyyyy \ IIJ_DO_SERVICE_CODE=zzzzzz \ -lego --dns iij -d '*.example.com' -d example.com run +lego --email you@example.com --dns iij --domains my.example.org run ``` @@ -44,19 +44,19 @@ lego --dns iij -d '*.example.com' -d example.com run | `IIJ_DO_SERVICE_CODE` | DO service code | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `IIJ_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 4) | -| `IIJ_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 240) | -| `IIJ_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `IIJ_POLLING_INTERVAL` | Time between DNS propagation check | +| `IIJ_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `IIJ_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_iijdpf.md b/docs/content/dns/zz_gen_iijdpf.md index 12e126f49..eb240a388 100644 --- a/docs/content/dns/zz_gen_iijdpf.md +++ b/docs/content/dns/zz_gen_iijdpf.md @@ -28,7 +28,7 @@ Here is an example bash command using the IIJ DNS Platform Service provider: ```bash IIJ_DPF_API_TOKEN=xxxxxxxx \ IIJ_DPF_DPM_SERVICE_CODE=yyyyyy \ -lego --dns iijdpf -d '*.example.com' -d example.com run +lego --email you@example.com --dns iijdpf --domains my.example.org run ``` @@ -42,7 +42,7 @@ lego --dns iijdpf -d '*.example.com' -d example.com run | `IIJ_DPF_DPM_SERVICE_CODE` | IIJ Managed DNS Service's service code | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -50,12 +50,12 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | Environment Variable Name | Description | |--------------------------------|-------------| | `IIJ_DPF_API_ENDPOINT` | API endpoint URL, defaults to https://api.dns-platform.jp/dpf/v1 | -| `IIJ_DPF_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 5) | -| `IIJ_DPF_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 660) | -| `IIJ_DPF_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `IIJ_DPF_POLLING_INTERVAL` | Time between DNS propagation check, defaults to 5 second | +| `IIJ_DPF_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation, defaults to 660 second | +| `IIJ_DPF_TTL` | The TTL of the TXT record used for the DNS challenge, default to 300 | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_infoblox.md b/docs/content/dns/zz_gen_infoblox.md index 74b80b2d1..2a703f119 100644 --- a/docs/content/dns/zz_gen_infoblox.md +++ b/docs/content/dns/zz_gen_infoblox.md @@ -29,7 +29,7 @@ Here is an example bash command using the Infoblox provider: INFOBLOX_USERNAME=api-user-529 \ INFOBLOX_PASSWORD=b9841238feb177a84330febba8a83208921177bffe733 \ INFOBLOX_HOST=infoblox.example.org -lego --dns infoblox -d '*.example.com' -d example.com run +lego --email you@example.com --dns infoblox --domains my.example.org run ``` @@ -44,25 +44,24 @@ lego --dns infoblox -d '*.example.com' -d example.com run | `INFOBLOX_USERNAME` | Account Username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `INFOBLOX_CA_CERTIFICATE` | The path to the CA certificate (PEM encoded) | -| `INFOBLOX_DNS_VIEW` | The view for the TXT records (Default: External) | -| `INFOBLOX_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `INFOBLOX_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `INFOBLOX_PORT` | The port for the infoblox grid manager (Default: 443) | -| `INFOBLOX_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `INFOBLOX_SSL_VERIFY` | Whether or not to verify the TLS certificate (Default: true) | -| `INFOBLOX_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | -| `INFOBLOX_WAPI_VERSION` | The version of WAPI being used (Default: 2.11) | +| `INFOBLOX_DNS_VIEW` | The view for the TXT records, default: External | +| `INFOBLOX_HTTP_TIMEOUT` | HTTP request timeout | +| `INFOBLOX_POLLING_INTERVAL` | Time between DNS propagation check | +| `INFOBLOX_PORT` | The port for the infoblox grid manager, default: 443 | +| `INFOBLOX_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `INFOBLOX_SSL_VERIFY` | Whether or not to verify the TLS certificate, default: true | +| `INFOBLOX_TTL` | The TTL of the TXT record used for the DNS challenge | +| `INFOBLOX_WAPI_VERSION` | The version of WAPI being used, default: 2.11 | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). When creating an API's user ensure it has the proper permissions for the view you are working with. diff --git a/docs/content/dns/zz_gen_infomaniak.md b/docs/content/dns/zz_gen_infomaniak.md index 7254241b1..695cb3701 100644 --- a/docs/content/dns/zz_gen_infomaniak.md +++ b/docs/content/dns/zz_gen_infomaniak.md @@ -27,7 +27,7 @@ Here is an example bash command using the Infomaniak provider: ```bash INFOMANIAK_ACCESS_TOKEN=1234567898765432 \ -lego --dns infomaniak -d '*.example.com' -d example.com run +lego --email you@example.com --dns infomaniak --domains my.example.org run ``` @@ -40,7 +40,7 @@ lego --dns infomaniak -d '*.example.com' -d example.com run | `INFOMANIAK_ACCESS_TOKEN` | Access token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -48,13 +48,13 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | Environment Variable Name | Description | |--------------------------------|-------------| | `INFOMANIAK_ENDPOINT` | https://api.infomaniak.com | -| `INFOMANIAK_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `INFOMANIAK_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | -| `INFOMANIAK_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `INFOMANIAK_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `INFOMANIAK_HTTP_TIMEOUT` | API request timeout | +| `INFOMANIAK_POLLING_INTERVAL` | Time between DNS propagation check | +| `INFOMANIAK_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `INFOMANIAK_TTL` | The TTL of the TXT record used for the DNS challenge in seconds | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Access token diff --git a/docs/content/dns/zz_gen_internetbs.md b/docs/content/dns/zz_gen_internetbs.md index f0d9df3c1..510d2279e 100644 --- a/docs/content/dns/zz_gen_internetbs.md +++ b/docs/content/dns/zz_gen_internetbs.md @@ -28,7 +28,7 @@ Here is an example bash command using the Internet.bs provider: ```bash INTERNET_BS_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx \ INTERNET_BS_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \ -lego --dns internetbs -d '*.example.com' -d example.com run +lego --email you@example.com --dns internetbs --domains my.example.org run ``` @@ -42,20 +42,20 @@ lego --dns internetbs -d '*.example.com' -d example.com run | `INTERNET_BS_PASSWORD` | API password | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `INTERNET_BS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `INTERNET_BS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `INTERNET_BS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `INTERNET_BS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) | +| `INTERNET_BS_HTTP_TIMEOUT` | API request timeout | +| `INTERNET_BS_POLLING_INTERVAL` | Time between DNS propagation check | +| `INTERNET_BS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `INTERNET_BS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_inwx.md b/docs/content/dns/zz_gen_inwx.md index 3e7d999e9..ef3a2b0f3 100644 --- a/docs/content/dns/zz_gen_inwx.md +++ b/docs/content/dns/zz_gen_inwx.md @@ -28,13 +28,13 @@ Here is an example bash command using the INWX provider: ```bash INWX_USERNAME=xxxxxxxxxx \ INWX_PASSWORD=yyyyyyyyyy \ -lego --dns inwx -d '*.example.com' -d example.com run +lego --email you@example.com --dns inwx --domains my.example.org run # 2FA INWX_USERNAME=xxxxxxxxxx \ INWX_PASSWORD=yyyyyyyyyy \ INWX_SHARED_SECRET=zzzzzzzzzz \ -lego --dns inwx -d '*.example.com' -d example.com run +lego --email you@example.com --dns inwx --domains my.example.org run ``` @@ -48,21 +48,21 @@ lego --dns inwx -d '*.example.com' -d example.com run | `INWX_USERNAME` | Username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `INWX_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `INWX_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 360) | +| `INWX_POLLING_INTERVAL` | Time between DNS propagation check | +| `INWX_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation (default 360s) | | `INWX_SANDBOX` | Activate the sandbox (boolean) | | `INWX_SHARED_SECRET` | shared secret related to 2FA | -| `INWX_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `INWX_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_ionos.md b/docs/content/dns/zz_gen_ionos.md index 78bd3ffb1..4698dc4f1 100644 --- a/docs/content/dns/zz_gen_ionos.md +++ b/docs/content/dns/zz_gen_ionos.md @@ -27,7 +27,7 @@ Here is an example bash command using the Ionos provider: ```bash IONOS_API_KEY=xxxxxxxx \ -lego --dns ionos -d '*.example.com' -d example.com run +lego --email you@example.com --dns ionos --domains my.example.org run ``` @@ -40,20 +40,20 @@ lego --dns ionos -d '*.example.com' -d example.com run | `IONOS_API_KEY` | API key `.` https://developer.hosting.ionos.com/docs/getstarted | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `IONOS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `IONOS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `IONOS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 900) | -| `IONOS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `IONOS_HTTP_TIMEOUT` | API request timeout | +| `IONOS_POLLING_INTERVAL` | Time between DNS propagation check | +| `IONOS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `IONOS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_ionoscloud.md b/docs/content/dns/zz_gen_ionoscloud.md deleted file mode 100644 index 6007670a7..000000000 --- a/docs/content/dns/zz_gen_ionoscloud.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: "Ionos Cloud" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: ionoscloud -dnsprovider: - since: "v4.30.0" - code: "ionoscloud" - url: "https://cloud.ionos.de/network/cloud-dns" ---- - - - - - - -Configuration for [Ionos Cloud](https://cloud.ionos.de/network/cloud-dns). - - - - -- Code: `ionoscloud` -- Since: v4.30.0 - - -Here is an example bash command using the Ionos Cloud provider: - -```bash -IONOSCLOUD_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns ionoscloud -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `IONOSCLOUD_API_TOKEN` | API token | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `IONOSCLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `IONOSCLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `IONOSCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `IONOSCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://api.ionos.com/docs/dns/v1/) - - - - diff --git a/docs/content/dns/zz_gen_ipv64.md b/docs/content/dns/zz_gen_ipv64.md index 00a0292a6..138cbffb7 100644 --- a/docs/content/dns/zz_gen_ipv64.md +++ b/docs/content/dns/zz_gen_ipv64.md @@ -27,7 +27,7 @@ Here is an example bash command using the IPv64 provider: ```bash IPV64_API_KEY=xxxxxx \ -lego --dns ipv64 -d '*.example.com' -d example.com run +lego --email you@example.com --dns ipv64 --domains my.example.org run ``` @@ -40,19 +40,20 @@ lego --dns ipv64 -d '*.example.com' -d example.com run | `IPV64_API_KEY` | Account API Key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `IPV64_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `IPV64_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `IPV64_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `IPV64_HTTP_TIMEOUT` | API request timeout | +| `IPV64_POLLING_INTERVAL` | Time between DNS propagation check | +| `IPV64_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `IPV64_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_ispconfig.md b/docs/content/dns/zz_gen_ispconfig.md deleted file mode 100644 index e56f1f0b1..000000000 --- a/docs/content/dns/zz_gen_ispconfig.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: "ISPConfig 3" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: ispconfig -dnsprovider: - since: "v4.31.0" - code: "ispconfig" - url: "https://www.ispconfig.org/" ---- - - - - - - -Configuration for [ISPConfig 3](https://www.ispconfig.org/). - - - - -- Code: `ispconfig` -- Since: v4.31.0 - - -Here is an example bash command using the ISPConfig 3 provider: - -```bash -ISPCONFIG_SERVER_URL="https://example.com:8080/remote/json.php" \ -ISPCONFIG_USERNAME="xxx" \ -ISPCONFIG_PASSWORD="yyy" \ -lego --dns ispconfig -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `ISPCONFIG_PASSWORD` | Password | -| `ISPCONFIG_SERVER_URL` | Server URL | -| `ISPCONFIG_USERNAME` | Username | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `ISPCONFIG_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `ISPCONFIG_INSECURE_SKIP_VERIFY` | Whether to verify the API certificate | -| `ISPCONFIG_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `ISPCONFIG_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `ISPCONFIG_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/index.html) - - - - diff --git a/docs/content/dns/zz_gen_ispconfigddns.md b/docs/content/dns/zz_gen_ispconfigddns.md deleted file mode 100644 index 3d1dd83c3..000000000 --- a/docs/content/dns/zz_gen_ispconfigddns.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: "ISPConfig 3 - Dynamic DNS (DDNS) Module" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: ispconfigddns -dnsprovider: - since: "v4.31.0" - code: "ispconfigddns" - url: "https://www.ispconfig.org/" ---- - - - - - - -Configuration for [ISPConfig 3 - Dynamic DNS (DDNS) Module](https://www.ispconfig.org/). - - - - -- Code: `ispconfigddns` -- Since: v4.31.0 - - -Here is an example bash command using the ISPConfig 3 - Dynamic DNS (DDNS) Module provider: - -```bash -ISPCONFIG_DDNS_SERVER_URL="https://panel.example.com:8080" \ -ISPCONFIG_DDNS_TOKEN=xxxxxx \ -lego --dns ispconfigddns -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `ISPCONFIG_DDNS_SERVER_URL` | API server URL (ex: https://panel.example.com:8080) | -| `ISPCONFIG_DDNS_TOKEN` | DDNS API token | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `ISPCONFIG_DDNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `ISPCONFIG_DDNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `ISPCONFIG_DDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `ISPCONFIG_DDNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - -ISPConfig DNS provider supports leveraging the [ISPConfig 3 Dynamic DNS (DDNS) Module](https://github.com/mhofer117/ispconfig-ddns-module). - -Requires the DDNS module described at https://www.ispconfig.org/ispconfig/download/ - -See https://www.howtoforge.com/community/threads/ispconfig-3-danymic-dns-ddns-module.87967/ for additional details. - - - -## More information - -- [API documentation](https://github.com/mhofer117/ispconfig-ddns-module/tree/master/lib/updater) - - - - diff --git a/docs/content/dns/zz_gen_iwantmyname.md b/docs/content/dns/zz_gen_iwantmyname.md index 4638e1379..34669d331 100644 --- a/docs/content/dns/zz_gen_iwantmyname.md +++ b/docs/content/dns/zz_gen_iwantmyname.md @@ -1,5 +1,5 @@ --- -title: "iwantmyname (Deprecated)" +title: "iwantmyname" date: 2019-03-03T16:39:46+01:00 draft: false slug: iwantmyname @@ -13,10 +13,8 @@ dnsprovider: -The iwantmyname API has shut down. - -https://github.com/go-acme/lego/issues/2563 +Configuration for [iwantmyname](https://iwantmyname.com). @@ -25,12 +23,12 @@ https://github.com/go-acme/lego/issues/2563 - Since: v4.7.0 -Here is an example bash command using the iwantmyname (Deprecated) provider: +Here is an example bash command using the iwantmyname provider: ```bash IWANTMYNAME_USERNAME=xxxxxxxx \ IWANTMYNAME_PASSWORD=xxxxxxxx \ -lego --dns iwantmyname -d '*.example.com' -d example.com run +lego --email you@example.com --dns iwantmyname --domains my.example.org run ``` @@ -44,20 +42,20 @@ lego --dns iwantmyname -d '*.example.com' -d example.com run | `IWANTMYNAME_USERNAME` | API username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `IWANTMYNAME_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `IWANTMYNAME_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `IWANTMYNAME_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `IWANTMYNAME_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `IWANTMYNAME_HTTP_TIMEOUT` | API request timeout | +| `IWANTMYNAME_POLLING_INTERVAL` | Time between DNS propagation check | +| `IWANTMYNAME_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `IWANTMYNAME_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_jdcloud.md b/docs/content/dns/zz_gen_jdcloud.md deleted file mode 100644 index a37cc3520..000000000 --- a/docs/content/dns/zz_gen_jdcloud.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: "JD Cloud" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: jdcloud -dnsprovider: - since: "v4.31.0" - code: "jdcloud" - url: "https://www.jdcloud.com/" ---- - - - - - - -Configuration for [JD Cloud](https://www.jdcloud.com/). - - - - -- Code: `jdcloud` -- Since: v4.31.0 - - -Here is an example bash command using the JD Cloud provider: - -```bash -JDCLOUD_ACCESS_KEY_ID="xxx" \ -JDCLOUD_ACCESS_KEY_SECRET="yyy" \ -lego --dns jdcloud -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `JDCLOUD_ACCESS_KEY_ID` | Access key ID | -| `JDCLOUD_ACCESS_KEY_SECRET` | Access key secret | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `JDCLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `JDCLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `JDCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `JDCLOUD_REGION_ID` | Region ID (Default: cn-north-1) | -| `JDCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://docs.jdcloud.com/cn/jd-cloud-dns/api/overview) -- [Go client](https://github.com/jdcloud-api/jdcloud-sdk-go) - - - - diff --git a/docs/content/dns/zz_gen_joker.md b/docs/content/dns/zz_gen_joker.md index a5ecd47de..f56fee17d 100644 --- a/docs/content/dns/zz_gen_joker.md +++ b/docs/content/dns/zz_gen_joker.md @@ -30,17 +30,17 @@ Here is an example bash command using the Joker provider: JOKER_API_MODE=SVC \ JOKER_USERNAME= \ JOKER_PASSWORD= \ -lego --dns joker -d '*.example.com' -d example.com run +lego --email you@example.com --dns joker --domains my.example.org run # DMAPI JOKER_API_MODE=DMAPI \ JOKER_USERNAME= \ JOKER_PASSWORD= \ -lego --dns joker -d '*.example.com' -d example.com run +lego --email you@example.com --dns joker --domains my.example.org run ## or JOKER_API_MODE=DMAPI \ JOKER_API_KEY= \ -lego --dns joker -d '*.example.com' -d example.com run +lego --email you@example.com --dns joker --domains my.example.org run ``` @@ -56,21 +56,21 @@ lego --dns joker -d '*.example.com' -d example.com run | `JOKER_USERNAME` | Joker.com username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `JOKER_HTTP_TIMEOUT` | API request timeout in seconds (Default: 60) | -| `JOKER_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `JOKER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `JOKER_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60), only with 'SVC' mode | -| `JOKER_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `JOKER_HTTP_TIMEOUT` | API request timeout | +| `JOKER_POLLING_INTERVAL` | Time between DNS propagation check | +| `JOKER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `JOKER_SEQUENCE_INTERVAL` | Time between sequential requests (only with 'SVC' mode) | +| `JOKER_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## SVC mode diff --git a/docs/content/dns/zz_gen_keyhelp.md b/docs/content/dns/zz_gen_keyhelp.md deleted file mode 100644 index e39d3ce82..000000000 --- a/docs/content/dns/zz_gen_keyhelp.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: "KeyHelp" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: keyhelp -dnsprovider: - since: "v4.26.0" - code: "keyhelp" - url: "https://www.keyweb.de/en/keyhelp/keyhelp/" ---- - - - - - - -Configuration for [KeyHelp](https://www.keyweb.de/en/keyhelp/keyhelp/). - - - - -- Code: `keyhelp` -- Since: v4.26.0 - - -Here is an example bash command using the KeyHelp provider: - -```bash -KEYHELP_BASE_URL="https://keyhelp.example.com" \ -KEYHELP_API_KEY="xxx" \ -lego --dns keyhelp -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `KEYHELP_API_KEY` | API key | -| `KEYHELP_BASE_URL` | Server URL | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `KEYHELP_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `KEYHELP_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `KEYHELP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `KEYHELP_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://app.swaggerhub.com/apis-docs/keyhelp/api/) - - - - diff --git a/docs/content/dns/zz_gen_leaseweb.md b/docs/content/dns/zz_gen_leaseweb.md deleted file mode 100644 index 13ded490a..000000000 --- a/docs/content/dns/zz_gen_leaseweb.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: "Leaseweb" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: leaseweb -dnsprovider: - since: "v4.32.0" - code: "leaseweb" - url: "https://www.leaseweb.com/en/" ---- - - - - - - -Configuration for [Leaseweb](https://www.leaseweb.com/en/). - - - - -- Code: `leaseweb` -- Since: v4.32.0 - - -Here is an example bash command using the Leaseweb provider: - -```bash -LEASEWEB_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns leaseweb -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `LEASEWEB_API_KEY` | API key | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `LEASEWEB_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `LEASEWEB_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `LEASEWEB_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `LEASEWEB_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://developer.leaseweb.com/docs/#tag/DNS) - - - - diff --git a/docs/content/dns/zz_gen_liara.md b/docs/content/dns/zz_gen_liara.md index 658ce8077..ebe5c2993 100644 --- a/docs/content/dns/zz_gen_liara.md +++ b/docs/content/dns/zz_gen_liara.md @@ -27,7 +27,7 @@ Here is an example bash command using the Liara provider: ```bash LIARA_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns liara -d '*.example.com' -d example.com run +lego --email myemail@example.com --dns liara --domains my.example.org run ``` @@ -40,28 +40,27 @@ lego --dns liara -d '*.example.com' -d example.com run | `LIARA_API_KEY` | The API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `LIARA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `LIARA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `LIARA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `LIARA_TEAM_ID` | The team ID to access services in a team | -| `LIARA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) | +| `LIARA_HTTP_TIMEOUT` | API request timeout | +| `LIARA_POLLING_INTERVAL` | Time between DNS propagation check | +| `LIARA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `LIARA_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information -- [API documentation](https://openapi.liara.ir/?urls.primaryName=DNS) +- [API documentation](https://dns-service.iran.liara.ir/swagger) diff --git a/docs/content/dns/zz_gen_lightsail.md b/docs/content/dns/zz_gen_lightsail.md index 8e738611b..d5bd035bc 100644 --- a/docs/content/dns/zz_gen_lightsail.md +++ b/docs/content/dns/zz_gen_lightsail.md @@ -39,7 +39,7 @@ _Please contribute by adding a CLI example._ | `DNS_ZONE` | Domain name of the DNS zone | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -47,11 +47,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | Environment Variable Name | Description | |--------------------------------|-------------| | `AWS_SHARED_CREDENTIALS_FILE` | Managed by the AWS client. Shared credentials file. | -| `LIGHTSAIL_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `LIGHTSAIL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | +| `LIGHTSAIL_POLLING_INTERVAL` | Time between DNS propagation check | +| `LIGHTSAIL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Description diff --git a/docs/content/dns/zz_gen_limacity.md b/docs/content/dns/zz_gen_limacity.md deleted file mode 100644 index 29bc6e0a7..000000000 --- a/docs/content/dns/zz_gen_limacity.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: "Lima-City" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: limacity -dnsprovider: - since: "v4.18.0" - code: "limacity" - url: "https://www.lima-city.de" ---- - - - - - - -Configuration for [Lima-City](https://www.lima-city.de). - - - - -- Code: `limacity` -- Since: v4.18.0 - - -Here is an example bash command using the Lima-City provider: - -```bash -LIMACITY_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns limacity -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `LIMACITY_API_KEY` | The API key | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `LIMACITY_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `LIMACITY_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 80) | -| `LIMACITY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 480) | -| `LIMACITY_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 90) | -| `LIMACITY_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://www.lima-city.de/hilfe/lima-city-api) - - - - diff --git a/docs/content/dns/zz_gen_linode.md b/docs/content/dns/zz_gen_linode.md index e41ba7cd9..b73b5ed6b 100644 --- a/docs/content/dns/zz_gen_linode.md +++ b/docs/content/dns/zz_gen_linode.md @@ -27,7 +27,7 @@ Here is an example bash command using the Linode (v4) provider: ```bash LINODE_TOKEN=xxxxx \ -lego --dns linode -d '*.example.com' -d example.com run +lego --email you@example.com --dns linode --domains my.example.org run ``` @@ -40,20 +40,20 @@ lego --dns linode -d '*.example.com' -d example.com run | `LINODE_TOKEN` | API token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `LINODE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `LINODE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 15) | -| `LINODE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `LINODE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `LINODE_HTTP_TIMEOUT` | API request timeout | +| `LINODE_POLLING_INTERVAL` | Time between DNS propagation check | +| `LINODE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `LINODE_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_liquidweb.md b/docs/content/dns/zz_gen_liquidweb.md index bd2ce63b6..60ce89518 100644 --- a/docs/content/dns/zz_gen_liquidweb.md +++ b/docs/content/dns/zz_gen_liquidweb.md @@ -28,7 +28,7 @@ Here is an example bash command using the Liquid Web provider: ```bash LWAPI_USERNAME=someuser \ LWAPI_PASSWORD=somepass \ -lego --dns liquidweb -d '*.example.com' -d example.com run +lego --email you@example.com --dns liquidweb --domains my.example.org run ``` @@ -42,22 +42,22 @@ lego --dns liquidweb -d '*.example.com' -d example.com run | `LWAPI_USERNAME` | Liquid Web API Username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `LWAPI_HTTP_TIMEOUT` | API request timeout in seconds (Default: 60) | -| `LWAPI_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `LWAPI_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `LWAPI_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `LWAPI_HTTP_TIMEOUT` | Maximum waiting time for the DNS records to be created (not verified) | +| `LWAPI_POLLING_INTERVAL` | Time between DNS propagation check | +| `LWAPI_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `LWAPI_TTL` | The TTL of the TXT record used for the DNS challenge | | `LWAPI_URL` | Liquid Web API endpoint | | `LWAPI_ZONE` | DNS Zone | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_loopia.md b/docs/content/dns/zz_gen_loopia.md index bb3120c00..013c51e1e 100644 --- a/docs/content/dns/zz_gen_loopia.md +++ b/docs/content/dns/zz_gen_loopia.md @@ -28,7 +28,7 @@ Here is an example bash command using the Loopia provider: ```bash LOOPIA_API_USER=xxxxxxxx \ LOOPIA_API_PASSWORD=yyyyyyyy \ -lego --dns loopia -d '*.example.com' -d example.com run +lego --email my@email.com --dns loopia --domains my.domain.com run ``` @@ -42,7 +42,7 @@ lego --dns loopia -d '*.example.com' -d example.com run | `LOOPIA_API_USER` | API username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -50,13 +50,13 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | Environment Variable Name | Description | |--------------------------------|-------------| | `LOOPIA_API_URL` | API endpoint. Ex: https://api.loopia.se/RPCSERV or https://api.loopia.rs/RPCSERV | -| `LOOPIA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 60) | -| `LOOPIA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2400) | -| `LOOPIA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `LOOPIA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `LOOPIA_HTTP_TIMEOUT` | API request timeout | +| `LOOPIA_POLLING_INTERVAL` | Time between DNS propagation check | +| `LOOPIA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `LOOPIA_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ### API user diff --git a/docs/content/dns/zz_gen_luadns.md b/docs/content/dns/zz_gen_luadns.md index 8bf718ba3..0f8eac956 100644 --- a/docs/content/dns/zz_gen_luadns.md +++ b/docs/content/dns/zz_gen_luadns.md @@ -28,7 +28,7 @@ Here is an example bash command using the LuaDNS provider: ```bash LUADNS_API_USERNAME=youremail \ LUADNS_API_TOKEN=xxxxxxxx \ -lego --dns luadns -d '*.example.com' -d example.com run +lego --email you@example.com --dns luadns --domains my.example.org run ``` @@ -42,20 +42,20 @@ lego --dns luadns -d '*.example.com' -d example.com run | `LUADNS_API_USERNAME` | Username (your email) | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `LUADNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `LUADNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `LUADNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `LUADNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `LUADNS_HTTP_TIMEOUT` | API request timeout | +| `LUADNS_POLLING_INTERVAL` | Time between DNS propagation check | +| `LUADNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `LUADNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_mailinabox.md b/docs/content/dns/zz_gen_mailinabox.md index 62a6bdb57..d87da7e7f 100644 --- a/docs/content/dns/zz_gen_mailinabox.md +++ b/docs/content/dns/zz_gen_mailinabox.md @@ -29,7 +29,7 @@ Here is an example bash command using the Mail-in-a-Box provider: MAILINABOX_EMAIL=user@example.com \ MAILINABOX_PASSWORD=yyyy \ MAILINABOX_BASE_URL=https://box.example.com \ -lego --dns mailinabox -d '*.example.com' -d example.com run +lego --email you@example.com --dns mailinabox --domains my.example.org run ``` @@ -44,19 +44,18 @@ lego --dns mailinabox -d '*.example.com' -d example.com run | `MAILINABOX_PASSWORD` | User password | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `MAILINABOX_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `MAILINABOX_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 4) | -| `MAILINABOX_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | +| `MAILINABOX_POLLING_INTERVAL` | Time between DNS propagation check | +| `MAILINABOX_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_manageengine.md b/docs/content/dns/zz_gen_manageengine.md deleted file mode 100644 index a39db8208..000000000 --- a/docs/content/dns/zz_gen_manageengine.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: "ManageEngine CloudDNS" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: manageengine -dnsprovider: - since: "v4.21.0" - code: "manageengine" - url: "https://clouddns.manageengine.com" ---- - - - - - - -Configuration for [ManageEngine CloudDNS](https://clouddns.manageengine.com). - - - - -- Code: `manageengine` -- Since: v4.21.0 - - -Here is an example bash command using the ManageEngine CloudDNS provider: - -```bash -MANAGEENGINE_CLIENT_ID="xxx" \ -MANAGEENGINE_CLIENT_SECRET="yyy" \ -lego --dns manageengine -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `MANAGEENGINE_CLIENT_ID` | Client ID | -| `MANAGEENGINE_CLIENT_SECRET` | Client Secret | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `MANAGEENGINE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `MANAGEENGINE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `MANAGEENGINE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation) - - - - diff --git a/docs/content/dns/zz_gen_manual.md b/docs/content/dns/zz_gen_manual.md deleted file mode 100644 index 832ccaf58..000000000 --- a/docs/content/dns/zz_gen_manual.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -title: "Manual" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: manual -dnsprovider: - since: "v0.3.0" - code: "manual" - url: "" ---- - - - - - -Solving the DNS-01 challenge using CLI prompt. - - - - -- Code: `manual` -- Since: v0.3.0 - - -Here is an example bash command using the Manual provider: - -```bash -lego --dns manual -d '*.example.com' -d example.com run -``` - - - - -## Example - -To start using the CLI prompt "provider", start lego with `--dns manual`: - -```console -$ lego --dns manual -d example.com run -``` - -What follows are a few log print-outs, interspersed with some prompts, asking for you to do perform some actions: - -```txt -No key found for account you@example.com. Generating a P256 key. -Saved key to ./.lego/accounts/acme-v02.api.letsencrypt.org/you@example.com/keys/you@example.com.key -Please review the TOS at https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf -Do you accept the TOS? Y/n -``` - -If you accept the linked Terms of Service, hit `Enter`. - -```txt -[INFO] acme: Registering account for you@example.com -!!!! HEADS UP !!!! - -Your account credentials have been saved in your -configuration directory at "./.lego/accounts". - -You should make a secure backup of this folder now. This -configuration directory will also contain private keys -generated by lego and certificates obtained from the ACME -server. Making regular backups of this folder is ideal. -[INFO] [example.com] acme: Obtaining bundled SAN certificate -[INFO] [example.com] AuthURL: https://acme-v02.api.letsencrypt.org/acme/authz-v3/2345678901 -[INFO] [example.com] acme: Could not find solver for: tls-alpn-01 -[INFO] [example.com] acme: Could not find solver for: http-01 -[INFO] [example.com] acme: use dns-01 solver -[INFO] [example.com] acme: Preparing to solve DNS-01 -lego: Please create the following TXT record in your example.com. zone: -_acme-challenge.example.com. 120 IN TXT "hX0dPkG6Gfs9hUvBAchQclkyyoEKbShbpvJ9mY5q2JQ" -lego: Press 'Enter' when you are done -``` - -Do as instructed, and create the TXT records, and hit `Enter`. - -```txt -[INFO] [example.com] acme: Trying to solve DNS-01 -[INFO] [example.com] acme: Checking DNS record propagation using [192.168.8.1:53] -[INFO] Wait for propagation [timeout: 1m0s, interval: 2s] -[INFO] [example.com] acme: Waiting for DNS record propagation. -[INFO] [example.com] The server validated our request -[INFO] [example.com] acme: Cleaning DNS-01 challenge -lego: You can now remove this TXT record from your example.com. zone: -_acme-challenge.example.com. 120 IN TXT "hX0dPkG6Gfs9hUvBAchQclkyyoEKbShbpvJ9mY5q2JQ" -[INFO] [example.com] acme: Validations succeeded; requesting certificates -[INFO] [example.com] Server responded with a certificate. -``` - -As mentioned, you can now remove the TXT record again. - - - - - - - - diff --git a/docs/content/dns/zz_gen_metaname.md b/docs/content/dns/zz_gen_metaname.md index 156cf15eb..1cad9b51d 100644 --- a/docs/content/dns/zz_gen_metaname.md +++ b/docs/content/dns/zz_gen_metaname.md @@ -28,7 +28,7 @@ Here is an example bash command using the Metaname provider: ```bash METANAME_ACCOUNT_REFERENCE=xxxx \ METANAME_API_KEY=yyyyyyy \ -lego --dns metaname -d '*.example.com' -d example.com run +lego --email you@example.com --dns metaname --domains my.example.org run ``` @@ -42,19 +42,19 @@ lego --dns metaname -d '*.example.com' -d example.com run | `METANAME_API_KEY` | API Key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `METANAME_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `METANAME_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `METANAME_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `METANAME_POLLING_INTERVAL` | Time between DNS propagation check | +| `METANAME_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `METANAME_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_metaregistrar.md b/docs/content/dns/zz_gen_metaregistrar.md deleted file mode 100644 index 22de046e2..000000000 --- a/docs/content/dns/zz_gen_metaregistrar.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: "Metaregistrar" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: metaregistrar -dnsprovider: - since: "v4.23.0" - code: "metaregistrar" - url: "https://metaregistrar.com/" ---- - - - - - - -Configuration for [Metaregistrar](https://metaregistrar.com/). - - - - -- Code: `metaregistrar` -- Since: v4.23.0 - - -Here is an example bash command using the Metaregistrar provider: - -```bash -METAREGISTRAR_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns metaregistrar -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `METAREGISTRAR_API_TOKEN` | The API token | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `METAREGISTRAR_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `METAREGISTRAR_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `METAREGISTRAR_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `METAREGISTRAR_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://metaregistrar.dev/docu/metaapi/) - - - - diff --git a/docs/content/dns/zz_gen_mijnhost.md b/docs/content/dns/zz_gen_mijnhost.md deleted file mode 100644 index 3d8f71aff..000000000 --- a/docs/content/dns/zz_gen_mijnhost.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: "mijn.host" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: mijnhost -dnsprovider: - since: "v4.18.0" - code: "mijnhost" - url: "https://mijn.host/" ---- - - - - - - -Configuration for [mijn.host](https://mijn.host/). - - - - -- Code: `mijnhost` -- Since: v4.18.0 - - -Here is an example bash command using the mijn.host provider: - -```bash -MIJNHOST_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns mijnhost -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `MIJNHOST_API_KEY` | The API key | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `MIJNHOST_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `MIJNHOST_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `MIJNHOST_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `MIJNHOST_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) | -| `MIJNHOST_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://mijn.host/api/doc/) - - - - diff --git a/docs/content/dns/zz_gen_mittwald.md b/docs/content/dns/zz_gen_mittwald.md deleted file mode 100644 index 7714ef54f..000000000 --- a/docs/content/dns/zz_gen_mittwald.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: "Mittwald" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: mittwald -dnsprovider: - since: "v1.48.0" - code: "mittwald" - url: "https://www.mittwald.de/" ---- - - - - - - -Configuration for [Mittwald](https://www.mittwald.de/). - - - - -- Code: `mittwald` -- Since: v1.48.0 - - -Here is an example bash command using the Mittwald provider: - -```bash -MITTWALD_TOKEN=my-token \ -lego --dns mittwald -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `MITTWALD_TOKEN` | API token | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `MITTWALD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `MITTWALD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | -| `MITTWALD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `MITTWALD_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 120) | -| `MITTWALD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://api.mittwald.de/v2/docs/) - - - - diff --git a/docs/content/dns/zz_gen_myaddr.md b/docs/content/dns/zz_gen_myaddr.md deleted file mode 100644 index 4a52a058b..000000000 --- a/docs/content/dns/zz_gen_myaddr.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: "myaddr.{tools,dev,io}" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: myaddr -dnsprovider: - since: "v4.22.0" - code: "myaddr" - url: "https://myaddr.tools/" ---- - - - - - - -Configuration for [myaddr.{tools,dev,io}](https://myaddr.tools/). - - - - -- Code: `myaddr` -- Since: v4.22.0 - - -Here is an example bash command using the myaddr.{tools,dev,io} provider: - -```bash -MYADDR_PRIVATE_KEYS_MAPPING="example:123,test:456" \ -lego --dns myaddr -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `MYADDR_PRIVATE_KEYS_MAPPING` | Mapping between subdomains and private keys. The format is: `:,:,:` | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `MYADDR_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `MYADDR_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `MYADDR_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `MYADDR_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 2) | -| `MYADDR_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://myaddr.tools/) - - - - diff --git a/docs/content/dns/zz_gen_mydnsjp.md b/docs/content/dns/zz_gen_mydnsjp.md index 0a49404bb..abc1b6384 100644 --- a/docs/content/dns/zz_gen_mydnsjp.md +++ b/docs/content/dns/zz_gen_mydnsjp.md @@ -28,7 +28,7 @@ Here is an example bash command using the MyDNS.jp provider: ```bash MYDNSJP_MASTER_ID=xxxxx \ MYDNSJP_PASSWORD=xxxxx \ -lego --dns mydnsjp -d '*.example.com' -d example.com run +lego --email you@example.com --dns mydnsjp --domains my.example.org run ``` @@ -42,19 +42,20 @@ lego --dns mydnsjp -d '*.example.com' -d example.com run | `MYDNSJP_PASSWORD` | Password | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `MYDNSJP_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `MYDNSJP_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `MYDNSJP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | +| `MYDNSJP_HTTP_TIMEOUT` | API request timeout | +| `MYDNSJP_POLLING_INTERVAL` | Time between DNS propagation check | +| `MYDNSJP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `MYDNSJP_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_mythicbeasts.md b/docs/content/dns/zz_gen_mythicbeasts.md index 70e38d249..97ff996c6 100644 --- a/docs/content/dns/zz_gen_mythicbeasts.md +++ b/docs/content/dns/zz_gen_mythicbeasts.md @@ -28,7 +28,7 @@ Here is an example bash command using the MythicBeasts provider: ```bash MYTHICBEASTS_USERNAME=myuser \ MYTHICBEASTS_PASSWORD=mypass \ -lego --dns mythicbeasts -d '*.example.com' -d example.com run +lego --email you@example.com --dns mythicbeasts --domains my.example.org run ``` @@ -42,7 +42,7 @@ lego --dns mythicbeasts -d '*.example.com' -d example.com run | `MYTHICBEASTS_USERNAME` | User name | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -51,13 +51,13 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). |--------------------------------|-------------| | `MYTHICBEASTS_API_ENDPOINT` | The endpoint for the API (must implement v2) | | `MYTHICBEASTS_AUTH_API_ENDPOINT` | The endpoint for Mythic Beasts' Authentication | -| `MYTHICBEASTS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | -| `MYTHICBEASTS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `MYTHICBEASTS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `MYTHICBEASTS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `MYTHICBEASTS_HTTP_TIMEOUT` | API request timeout | +| `MYTHICBEASTS_POLLING_INTERVAL` | Time between DNS propagation check | +| `MYTHICBEASTS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `MYTHICBEASTS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). If you are using specific API keys, then the username is the API ID for your API key, and the password is the API secret. diff --git a/docs/content/dns/zz_gen_namecheap.md b/docs/content/dns/zz_gen_namecheap.md index 9d7143d84..20962d87d 100644 --- a/docs/content/dns/zz_gen_namecheap.md +++ b/docs/content/dns/zz_gen_namecheap.md @@ -33,7 +33,7 @@ Here is an example bash command using the Namecheap provider: ```bash NAMECHEAP_API_USER=user \ NAMECHEAP_API_KEY=key \ -lego --dns namecheap -d '*.example.com' -d example.com run +lego --email you@example.com --dns namecheap --domains my.example.org run ``` @@ -47,21 +47,21 @@ lego --dns namecheap -d '*.example.com' -d example.com run | `NAMECHEAP_API_USER` | API user | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `NAMECHEAP_HTTP_TIMEOUT` | API request timeout in seconds (Default: 60) | -| `NAMECHEAP_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 15) | -| `NAMECHEAP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 3600) | +| `NAMECHEAP_HTTP_TIMEOUT` | API request timeout | +| `NAMECHEAP_POLLING_INTERVAL` | Time between DNS propagation check | +| `NAMECHEAP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `NAMECHEAP_SANDBOX` | Activate the sandbox (boolean) | -| `NAMECHEAP_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `NAMECHEAP_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_namedotcom.md b/docs/content/dns/zz_gen_namedotcom.md index 2860ff0ae..2bff94b52 100644 --- a/docs/content/dns/zz_gen_namedotcom.md +++ b/docs/content/dns/zz_gen_namedotcom.md @@ -28,7 +28,7 @@ Here is an example bash command using the Name.com provider: ```bash NAMECOM_USERNAME=foo.bar \ NAMECOM_API_TOKEN=a379a6f6eeafb9a55e378c118034e2751e682fab \ -lego --dns namedotcom -d '*.example.com' -d example.com run +lego --email you@example.com --dns namedotcom --domains my.example.org run ``` @@ -42,20 +42,20 @@ lego --dns namedotcom -d '*.example.com' -d example.com run | `NAMECOM_USERNAME` | Username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `NAMECOM_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | -| `NAMECOM_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 20) | -| `NAMECOM_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 900) | -| `NAMECOM_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `NAMECOM_HTTP_TIMEOUT` | API request timeout | +| `NAMECOM_POLLING_INTERVAL` | Time between DNS propagation check | +| `NAMECOM_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `NAMECOM_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_namesilo.md b/docs/content/dns/zz_gen_namesilo.md index 207a1603f..4e4cbd44e 100644 --- a/docs/content/dns/zz_gen_namesilo.md +++ b/docs/content/dns/zz_gen_namesilo.md @@ -27,7 +27,7 @@ Here is an example bash command using the Namesilo provider: ```bash NAMESILO_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \ -lego --dns namesilo -d '*.example.com' -d example.com run +lego --email you@example.com --dns namesilo --domains my.example.org run ``` @@ -40,19 +40,19 @@ lego --dns namesilo -d '*.example.com' -d example.com run | `NAMESILO_API_KEY` | Client ID | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `NAMESILO_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `NAMESILO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60), it is better to set larger than 15 minutes | -| `NAMESILO_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600), should be in [3600, 2592000] | +| `NAMESILO_POLLING_INTERVAL` | Time between DNS propagation check | +| `NAMESILO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation, it is better to set larger than 15m | +| `NAMESILO_TTL` | The TTL of the TXT record used for the DNS challenge, should be in [3600, 2592000] | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_namesurfer.md b/docs/content/dns/zz_gen_namesurfer.md deleted file mode 100644 index 9a2802d0e..000000000 --- a/docs/content/dns/zz_gen_namesurfer.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -title: "FusionLayer NameSurfer" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: namesurfer -dnsprovider: - since: "v4.32.0" - code: "namesurfer" - url: "https://www.fusionlayer.com/" ---- - - - - - - -Configuration for [FusionLayer NameSurfer](https://www.fusionlayer.com/). - - - - -- Code: `namesurfer` -- Since: v4.32.0 - - -Here is an example bash command using the FusionLayer NameSurfer provider: - -```bash -NAMESURFER_BASE_URL=https://foo.example.com:8443/API/NSService_10 \ -NAMESURFER_API_KEY=xxx \ -NAMESURFER_API_SECRET=yyy \ -lego --dns namesurfer -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `NAMESURFER_API_KEY` | API key name | -| `NAMESURFER_API_SECRET` | API secret | -| `NAMESURFER_BASE_URL` | The base URL of NameSurfer API (jsonrpc10) endpoint URL (e.g., https://foo.example.com:8443/API/NSService_10) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `NAMESURFER_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `NAMESURFER_INSECURE_SKIP_VERIFY` | Whether to verify the API certificate | -| `NAMESURFER_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `NAMESURFER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `NAMESURFER_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | -| `NAMESURFER_VIEW` | DNS view name (optional, default: empty string) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://web.archive.org/web/20260213170737/http://95.128.3.201:8053/API/NSService_10) - - - - diff --git a/docs/content/dns/zz_gen_nearlyfreespeech.md b/docs/content/dns/zz_gen_nearlyfreespeech.md index 31402d2d2..dd3745fce 100644 --- a/docs/content/dns/zz_gen_nearlyfreespeech.md +++ b/docs/content/dns/zz_gen_nearlyfreespeech.md @@ -28,7 +28,7 @@ Here is an example bash command using the NearlyFreeSpeech.NET provider: ```bash NEARLYFREESPEECH_API_KEY=xxxxxx \ NEARLYFREESPEECH_LOGIN=xxxx \ -lego --dns nearlyfreespeech -d '*.example.com' -d example.com run +lego --email you@example.com --dns nearlyfreespeech --domains my.example.org run ``` @@ -42,21 +42,21 @@ lego --dns nearlyfreespeech -d '*.example.com' -d example.com run | `NEARLYFREESPEECH_LOGIN` | Username for API requests | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `NEARLYFREESPEECH_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `NEARLYFREESPEECH_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `NEARLYFREESPEECH_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `NEARLYFREESPEECH_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) | -| `NEARLYFREESPEECH_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) | +| `NEARLYFREESPEECH_HTTP_TIMEOUT` | API request timeout | +| `NEARLYFREESPEECH_POLLING_INTERVAL` | Time between DNS propagation check | +| `NEARLYFREESPEECH_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `NEARLYFREESPEECH_SEQUENCE_INTERVAL` | Time between sequential requests | +| `NEARLYFREESPEECH_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_neodigit.md b/docs/content/dns/zz_gen_neodigit.md deleted file mode 100644 index aefeef4bf..000000000 --- a/docs/content/dns/zz_gen_neodigit.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: "Neodigit" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: neodigit -dnsprovider: - since: "v4.30.0" - code: "neodigit" - url: "https://www.neodigit.net" ---- - - - - - - -Configuration for [Neodigit](https://www.neodigit.net). - - - - -- Code: `neodigit` -- Since: v4.30.0 - - -Here is an example bash command using the Neodigit provider: - -```bash -NEODIGIT_TOKEN=xxxxxx \ -lego --dns neodigit -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `NEODIGIT_TOKEN` | API token | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `NEODIGIT_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `NEODIGIT_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | -| `NEODIGIT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) | -| `NEODIGIT_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://developers.neodigit.net/#dns) - - - - diff --git a/docs/content/dns/zz_gen_netcup.md b/docs/content/dns/zz_gen_netcup.md index 29def3285..4a19f1270 100644 --- a/docs/content/dns/zz_gen_netcup.md +++ b/docs/content/dns/zz_gen_netcup.md @@ -29,7 +29,7 @@ Here is an example bash command using the Netcup provider: NETCUP_CUSTOMER_NUMBER=xxxx \ NETCUP_API_KEY=yyyy \ NETCUP_API_PASSWORD=zzzz \ -lego --dns netcup -d '*.example.com' -d example.com run +lego --email you@example.com --dns netcup --domains my.example.org run ``` @@ -44,19 +44,20 @@ lego --dns netcup -d '*.example.com' -d example.com run | `NETCUP_CUSTOMER_NUMBER` | Customer number | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `NETCUP_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | -| `NETCUP_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 30) | -| `NETCUP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 900) | +| `NETCUP_HTTP_TIMEOUT` | API request timeout | +| `NETCUP_POLLING_INTERVAL` | Time between DNS propagation check | +| `NETCUP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `NETCUP_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_netlify.md b/docs/content/dns/zz_gen_netlify.md index 76651d9ef..dd82bd875 100644 --- a/docs/content/dns/zz_gen_netlify.md +++ b/docs/content/dns/zz_gen_netlify.md @@ -27,7 +27,7 @@ Here is an example bash command using the Netlify provider: ```bash NETLIFY_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --dns netlify -d '*.example.com' -d example.com run +lego --email you@example.com --dns netlify --domains my.example.org run ``` @@ -40,20 +40,20 @@ lego --dns netlify -d '*.example.com' -d example.com run | `NETLIFY_TOKEN` | Token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `NETLIFY_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `NETLIFY_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `NETLIFY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `NETLIFY_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `NETLIFY_HTTP_TIMEOUT` | API request timeout | +| `NETLIFY_POLLING_INTERVAL` | Time between DNS propagation check | +| `NETLIFY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `NETLIFY_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_nicmanager.md b/docs/content/dns/zz_gen_nicmanager.md index a29d72120..ab4bd674e 100644 --- a/docs/content/dns/zz_gen_nicmanager.md +++ b/docs/content/dns/zz_gen_nicmanager.md @@ -34,7 +34,7 @@ NICMANAGER_API_PASSWORD = "password" \ # Optionally, if your account has TOTP enabled, set the secret here NICMANAGER_API_OTP = "long-secret" \ -lego --dns nicmanager -d '*.example.com' -d example.com run +lego --email you@example.com --dns nicmanager --domains my.example.org run ## Login using account name + username @@ -45,7 +45,7 @@ NICMANAGER_API_PASSWORD = "password" \ # Optionally, if your account has TOTP enabled, set the secret here NICMANAGER_API_OTP = "long-secret" \ -lego --dns nicmanager -d '*.example.com' -d example.com run +lego --email you@example.com --dns nicmanager --domains my.example.org run ``` @@ -61,27 +61,27 @@ lego --dns nicmanager -d '*.example.com' -d example.com run | `NICMANAGER_API_USERNAME` | Username, used for Username-based login | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `NICMANAGER_API_MODE` | mode: 'anycast' or 'zones' (for FreeDNS) (default: 'anycast') | +| `NICMANAGER_API_MODE` | mode: 'anycast' or 'zone' (default: 'anycast') | | `NICMANAGER_API_OTP` | TOTP Secret (optional) | -| `NICMANAGER_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | -| `NICMANAGER_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `NICMANAGER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) | -| `NICMANAGER_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 900) | +| `NICMANAGER_HTTP_TIMEOUT` | API request timeout | +| `NICMANAGER_POLLING_INTERVAL` | Time between DNS propagation check | +| `NICMANAGER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `NICMANAGER_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Description You can log in using your account name + username or using your email address. -Optionally, if TOTP is configured for your account, set `NICMANAGER_API_OTP`. +Optionally if TOTP is configured for your account, set `NICMANAGER_API_OTP`. diff --git a/docs/content/dns/zz_gen_nicru.md b/docs/content/dns/zz_gen_nicru.md deleted file mode 100644 index 3ac8d99cf..000000000 --- a/docs/content/dns/zz_gen_nicru.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: "RU CENTER" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: nicru -dnsprovider: - since: "v4.24.0" - code: "nicru" - url: "https://nic.ru/" ---- - - - - - - -Configuration for [RU CENTER](https://nic.ru/). - - - - -- Code: `nicru` -- Since: v4.24.0 - - -Here is an example bash command using the RU CENTER provider: - -```bash -NICRU_USER="" \ -NICRU_PASSWORD="" \ -NICRU_SERVICE_ID="" \ -NICRU_SECRET="" \ -lego --dns nicru -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `NICRU_PASSWORD` | Password for an account in RU CENTER | -| `NICRU_SECRET` | Secret for application in DNS-hosting RU CENTER | -| `NICRU_SERVICE_ID` | Service ID for application in DNS-hosting RU CENTER | -| `NICRU_SERVICE_NAME` | Service Name for DNS-hosting RU CENTER | -| `NICRU_USER` | Agreement for an account in RU CENTER | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `NICRU_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 60) | -| `NICRU_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 600) | -| `NICRU_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 30) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - -## Credential information - -You can find information about service ID and secret https://www.nic.ru/manager/oauth.cgi?step=oauth.app_list - -| ENV Variable | Parameter from page | Example | -|---------------------|--------------------------------|-------------------| -| NICRU_USER | Username (Number of agreement) | NNNNNNN/NIC-D | -| NICRU_PASSWORD | Password account | | -| NICRU_SERVICE_ID | Application ID | hex-based, len 32 | -| NICRU_SECRET | Identity endpoint | string len 91 | - - - -## More information - -- [API documentation](https://www.nic.ru/help/api-dns-hostinga_3643.html) - - - - diff --git a/docs/content/dns/zz_gen_nifcloud.md b/docs/content/dns/zz_gen_nifcloud.md index 66f38223b..a5afebfa4 100644 --- a/docs/content/dns/zz_gen_nifcloud.md +++ b/docs/content/dns/zz_gen_nifcloud.md @@ -28,7 +28,7 @@ Here is an example bash command using the NIFCloud provider: ```bash NIFCLOUD_ACCESS_KEY_ID=xxxx \ NIFCLOUD_SECRET_ACCESS_KEY=yyyy \ -lego --dns nifcloud -d '*.example.com' -d example.com run +lego --email you@example.com --dns nifcloud --domains my.example.org run ``` @@ -42,20 +42,20 @@ lego --dns nifcloud -d '*.example.com' -d example.com run | `NIFCLOUD_SECRET_ACCESS_KEY` | Secret access key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `NIFCLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `NIFCLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `NIFCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `NIFCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `NIFCLOUD_HTTP_TIMEOUT` | API request timeout | +| `NIFCLOUD_POLLING_INTERVAL` | Time between DNS propagation check | +| `NIFCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `NIFCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_njalla.md b/docs/content/dns/zz_gen_njalla.md index 9a312df8b..a570fd356 100644 --- a/docs/content/dns/zz_gen_njalla.md +++ b/docs/content/dns/zz_gen_njalla.md @@ -27,7 +27,7 @@ Here is an example bash command using the Njalla provider: ```bash NJALLA_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --dns njalla -d '*.example.com' -d example.com run +lego --email you@example.com --dns njalla --domains my.example.org run ``` @@ -40,20 +40,20 @@ lego --dns njalla -d '*.example.com' -d example.com run | `NJALLA_TOKEN` | API token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `NJALLA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `NJALLA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `NJALLA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `NJALLA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `NJALLA_HTTP_TIMEOUT` | API request timeout | +| `NJALLA_POLLING_INTERVAL` | Time between DNS propagation check | +| `NJALLA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `NJALLA_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_nodion.md b/docs/content/dns/zz_gen_nodion.md index 8d61eb834..a1839c83d 100644 --- a/docs/content/dns/zz_gen_nodion.md +++ b/docs/content/dns/zz_gen_nodion.md @@ -27,7 +27,7 @@ Here is an example bash command using the Nodion provider: ```bash NODION_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns nodion -d '*.example.com' -d example.com run +lego --email myemail@example.com --dns nodion --domains my.example.org run ``` @@ -40,20 +40,20 @@ lego --dns nodion -d '*.example.com' -d example.com run | `NODION_API_TOKEN` | The API token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `NODION_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `NODION_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `NODION_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `NODION_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `NODION_HTTP_TIMEOUT` | API request timeout | +| `NODION_POLLING_INTERVAL` | Time between DNS propagation check | +| `NODION_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `NODION_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_ns1.md b/docs/content/dns/zz_gen_ns1.md index b2262169d..6394f5f2a 100644 --- a/docs/content/dns/zz_gen_ns1.md +++ b/docs/content/dns/zz_gen_ns1.md @@ -27,7 +27,7 @@ Here is an example bash command using the NS1 provider: ```bash NS1_API_KEY=xxxx \ -lego --dns ns1 -d '*.example.com' -d example.com run +lego --email you@example.com --dns ns1 --domains my.example.org run ``` @@ -40,20 +40,20 @@ lego --dns ns1 -d '*.example.com' -d example.com run | `NS1_API_KEY` | API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `NS1_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | -| `NS1_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `NS1_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `NS1_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `NS1_HTTP_TIMEOUT` | API request timeout | +| `NS1_POLLING_INTERVAL` | Time between DNS propagation check | +| `NS1_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `NS1_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_octenium.md b/docs/content/dns/zz_gen_octenium.md deleted file mode 100644 index f25da4f44..000000000 --- a/docs/content/dns/zz_gen_octenium.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: "Octenium" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: octenium -dnsprovider: - since: "v4.27.0" - code: "octenium" - url: "https://octenium.com/" ---- - - - - - - -Configuration for [Octenium](https://octenium.com/). - - - - -- Code: `octenium` -- Since: v4.27.0 - - -Here is an example bash command using the Octenium provider: - -```bash -OCTENIUM_API_KEY="xxx" \ -lego --dns octenium -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `OCTENIUM_API_KEY` | API key | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `OCTENIUM_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `OCTENIUM_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `OCTENIUM_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `OCTENIUM_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://octenium.com/api#tag/Domains-DNS) - - - - diff --git a/docs/content/dns/zz_gen_oraclecloud.md b/docs/content/dns/zz_gen_oraclecloud.md index b7192f380..9828c0285 100644 --- a/docs/content/dns/zz_gen_oraclecloud.md +++ b/docs/content/dns/zz_gen_oraclecloud.md @@ -26,21 +26,14 @@ Configuration for [Oracle Cloud](https://cloud.oracle.com/home). Here is an example bash command using the Oracle Cloud provider: ```bash -# Using API Key authentication: -OCI_PRIVATE_KEY_PATH="~/.oci/oci_api_key.pem" \ -OCI_PRIVATE_KEY_PASSWORD="secret" \ +OCI_PRIVKEY_FILE="~/.oci/oci_api_key.pem" \ +OCI_PRIVKEY_PASS="secret" \ OCI_TENANCY_OCID="ocid1.tenancy.oc1..secret" \ OCI_USER_OCID="ocid1.user.oc1..secret" \ -OCI_FINGERPRINT="00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00" \ +OCI_PUBKEY_FINGERPRINT="00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00" \ OCI_REGION="us-phoenix-1" \ OCI_COMPARTMENT_OCID="ocid1.tenancy.oc1..secret" \ -lego --dns oraclecloud -d '*.example.com' -d example.com run - -# Using Instance Principal authentication (when running on OCI compute instances): -# https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm -OCI_AUTH_TYPE="instance_principal" \ -OCI_COMPARTMENT_OCID="ocid1.tenancy.oc1..secret" \ -lego --dns oraclecloud -d '*.example.com' -d example.com run +lego --email you@example.com --dns oraclecloud --domains my.example.org run ``` @@ -51,34 +44,27 @@ lego --dns oraclecloud -d '*.example.com' -d example.com run | Environment Variable Name | Description | |-----------------------|-------------| | `OCI_COMPARTMENT_OCID` | Compartment OCID | -| `OCI_FINGERPRINT` | Public key fingerprint (ignored if `OCI_AUTH_TYPE=instance_principal`) | -| `OCI_PRIVATE_KEY_PASSWORD` | Private key password (ignored if `OCI_AUTH_TYPE=instance_principal`) | -| `OCI_PRIVATE_KEY_PATH` | Private key file (ignored if `OCI_AUTH_TYPE=instance_principal`) | -| `OCI_REGION` | Region (it can be empty if `OCI_AUTH_TYPE=instance_principal`). | -| `OCI_TENANCY_OCID` | Tenancy OCID (ignored if `OCI_AUTH_TYPE=instance_principal`) | -| `OCI_USER_OCID` | User OCID (ignored if `OCI_AUTH_TYPE=instance_principal`) | +| `OCI_PRIVKEY_FILE` | Private key file | +| `OCI_PRIVKEY_PASS` | Private key password | +| `OCI_PUBKEY_FINGERPRINT` | Public key fingerprint | +| `OCI_REGION` | Region | +| `OCI_TENANCY_OCID` | Tenancy OCID | +| `OCI_USER_OCID` | User OCID | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `OCI_AUTH_TYPE` | Authorization type. Possible values: 'instance_principal', '' (Default: '') | -| `OCI_HTTP_TIMEOUT` | API request timeout in seconds (Default: 60) | -| `OCI_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `OCI_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `OCI_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | -| `TF_VAR_fingerprint` | Alias on `OCI_FINGERPRINT` | -| `TF_VAR_private_key_path` | Alias on `OCI_PRIVATE_KEY_PATH` | -| `TF_VAR_region` | Alias on `OCI_REGION` | -| `TF_VAR_tenancy_ocid` | Alias on `OCI_TENANCY_OCID` | -| `TF_VAR_user_ocid` | Alias on `OCI_USER_OCID` | +| `OCI_POLLING_INTERVAL` | Time between DNS propagation check | +| `OCI_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `OCI_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_otc.md b/docs/content/dns/zz_gen_otc.md index 9da69c694..7d603a879 100644 --- a/docs/content/dns/zz_gen_otc.md +++ b/docs/content/dns/zz_gen_otc.md @@ -23,15 +23,9 @@ Configuration for [Open Telekom Cloud](https://cloud.telekom.de/en). - Since: v0.4.1 -Here is an example bash command using the Open Telekom Cloud provider: - -```bash -OTC_DOMAIN_NAME=domain_name \ -OTC_USER_NAME=user_name \ -OTC_PASSWORD=password \ -OTC_PROJECT_NAME=project_name \ -lego --dns otc -d '*.example.com' -d example.com run -``` +{{% notice note %}} +_Please contribute by adding a CLI example._ +{{% /notice %}} @@ -41,28 +35,27 @@ lego --dns otc -d '*.example.com' -d example.com run | Environment Variable Name | Description | |-----------------------|-------------| | `OTC_DOMAIN_NAME` | Domain name | +| `OTC_IDENTITY_ENDPOINT` | Identity endpoint URL | | `OTC_PASSWORD` | Password | | `OTC_PROJECT_NAME` | Project name | | `OTC_USER_NAME` | User name | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `OTC_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | -| `OTC_IDENTITY_ENDPOINT` | Identity endpoint URL (default: https://iam.eu-de.otc.t-systems.com:443/v3/auth/tokens) | -| `OTC_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `OTC_PRIVATE_ZONE` | Set to true to use private zones only (default: use public zones only) | -| `OTC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `OTC_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) | -| `OTC_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `OTC_HTTP_TIMEOUT` | API request timeout | +| `OTC_POLLING_INTERVAL` | Time between DNS propagation check | +| `OTC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `OTC_SEQUENCE_INTERVAL` | Time between sequential requests | +| `OTC_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_ovh.md b/docs/content/dns/zz_gen_ovh.md index aaafded85..724fb0cb6 100644 --- a/docs/content/dns/zz_gen_ovh.md +++ b/docs/content/dns/zz_gen_ovh.md @@ -32,20 +32,14 @@ OVH_APPLICATION_KEY=1234567898765432 \ OVH_APPLICATION_SECRET=b9841238feb177a84330febba8a832089 \ OVH_CONSUMER_KEY=256vfsd347245sdfg \ OVH_ENDPOINT=ovh-eu \ -lego --dns ovh -d '*.example.com' -d example.com run - -# Or Access Token: - -OVH_ACCESS_TOKEN=xxx \ -OVH_ENDPOINT=ovh-eu \ -lego --dns ovh -d '*.example.com' -d example.com run +lego --email you@example.com --dns ovh --domains my.example.org run # Or OAuth2: OVH_CLIENT_ID=yyy \ OVH_CLIENT_SECRET=xxx \ OVH_ENDPOINT=ovh-eu \ -lego --dns ovh -d '*.example.com' -d example.com run +lego --email you@example.com --dns ovh --domains my.example.org run ``` @@ -55,7 +49,6 @@ lego --dns ovh -d '*.example.com' -d example.com run | Environment Variable Name | Description | |-----------------------|-------------| -| `OVH_ACCESS_TOKEN` | Access token | | `OVH_APPLICATION_KEY` | Application key (Application Key authentication) | | `OVH_APPLICATION_SECRET` | Application secret (Application Key authentication) | | `OVH_CLIENT_ID` | Client ID (OAuth2) | @@ -64,20 +57,20 @@ lego --dns ovh -d '*.example.com' -d example.com run | `OVH_ENDPOINT` | Endpoint URL (ovh-eu or ovh-ca) | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `OVH_HTTP_TIMEOUT` | API request timeout in seconds (Default: 180) | -| `OVH_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `OVH_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `OVH_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `OVH_HTTP_TIMEOUT` | API request timeout | +| `OVH_POLLING_INTERVAL` | Time between DNS propagation check | +| `OVH_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `OVH_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Application Key and Secret diff --git a/docs/content/dns/zz_gen_pdns.md b/docs/content/dns/zz_gen_pdns.md index 7c2a8c663..22a730b07 100644 --- a/docs/content/dns/zz_gen_pdns.md +++ b/docs/content/dns/zz_gen_pdns.md @@ -28,7 +28,7 @@ Here is an example bash command using the PowerDNS provider: ```bash PDNS_API_URL=http://pdns-server:80/ \ PDNS_API_KEY=xxxx \ -lego --dns pdns -d '*.example.com' -d example.com run +lego --email you@example.com --dns pdns --domains my.example.org run ``` @@ -42,7 +42,7 @@ lego --dns pdns -d '*.example.com' -d example.com run | `PDNS_API_URL` | API URL | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -50,14 +50,14 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | Environment Variable Name | Description | |--------------------------------|-------------| | `PDNS_API_VERSION` | Skip API version autodetection and use the provided version number. | -| `PDNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `PDNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `PDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | +| `PDNS_HTTP_TIMEOUT` | API request timeout | +| `PDNS_POLLING_INTERVAL` | Time between DNS propagation check | +| `PDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `PDNS_SERVER_NAME` | Name of the server in the URL, 'localhost' by default | -| `PDNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `PDNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Information diff --git a/docs/content/dns/zz_gen_plesk.md b/docs/content/dns/zz_gen_plesk.md index 73ec9a55d..beb092d79 100644 --- a/docs/content/dns/zz_gen_plesk.md +++ b/docs/content/dns/zz_gen_plesk.md @@ -29,7 +29,7 @@ Here is an example bash command using the plesk.com provider: PLESK_SERVER_BASE_URL="https://plesk.myserver.com:8443" \ PLESK_USERNAME=xxxxxx \ PLESK_PASSWORD=yyyyyy \ -lego --dns plesk -d '*.example.com' -d example.com run +lego --email you@example.com --dns plesk --domains my.example.org run ``` @@ -44,20 +44,20 @@ lego --dns plesk -d '*.example.com' -d example.com run | `PLESK_USERNAME` | API username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `PLESK_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `PLESK_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `PLESK_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `PLESK_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `PLESK_HTTP_TIMEOUT` | API request timeout | +| `PLESK_POLLING_INTERVAL` | Time between DNS propagation check | +| `PLESK_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `PLESK_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_porkbun.md b/docs/content/dns/zz_gen_porkbun.md index f54e6f688..7c0567bc5 100644 --- a/docs/content/dns/zz_gen_porkbun.md +++ b/docs/content/dns/zz_gen_porkbun.md @@ -28,7 +28,7 @@ Here is an example bash command using the Porkbun provider: ```bash PORKBUN_SECRET_API_KEY=xxxxxx \ PORKBUN_API_KEY=yyyyyy \ -lego --dns porkbun -d '*.example.com' -d example.com run +lego --email you@example.com --dns porkbun --domains my.example.org run ``` @@ -42,20 +42,20 @@ lego --dns porkbun -d '*.example.com' -d example.com run | `PORKBUN_SECRET_API_KEY` | secret API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `PORKBUN_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `PORKBUN_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | -| `PORKBUN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 600) | -| `PORKBUN_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `PORKBUN_HTTP_TIMEOUT` | API request timeout | +| `PORKBUN_POLLING_INTERVAL` | Time between DNS propagation check | +| `PORKBUN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `PORKBUN_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_rackspace.md b/docs/content/dns/zz_gen_rackspace.md index b9a2ab710..f7c842e96 100644 --- a/docs/content/dns/zz_gen_rackspace.md +++ b/docs/content/dns/zz_gen_rackspace.md @@ -28,7 +28,7 @@ Here is an example bash command using the Rackspace provider: ```bash RACKSPACE_USER=xxxx \ RACKSPACE_API_KEY=yyyy \ -lego --dns rackspace -d '*.example.com' -d example.com run +lego --email you@example.com --dns rackspace --domains my.example.org run ``` @@ -42,20 +42,20 @@ lego --dns rackspace -d '*.example.com' -d example.com run | `RACKSPACE_USER` | API user | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `RACKSPACE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `RACKSPACE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 3) | -| `RACKSPACE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `RACKSPACE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `RACKSPACE_HTTP_TIMEOUT` | API request timeout | +| `RACKSPACE_POLLING_INTERVAL` | Time between DNS propagation check | +| `RACKSPACE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `RACKSPACE_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_rainyun.md b/docs/content/dns/zz_gen_rainyun.md deleted file mode 100644 index 680eb845a..000000000 --- a/docs/content/dns/zz_gen_rainyun.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: "Rain Yun/雨云" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: rainyun -dnsprovider: - since: "v4.21.0" - code: "rainyun" - url: "https://www.rainyun.com" ---- - - - - - - -Configuration for [Rain Yun/雨云](https://www.rainyun.com). - - - - -- Code: `rainyun` -- Since: v4.21.0 - - -Here is an example bash command using the Rain Yun/雨云 provider: - -```bash -RAINYUN_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns rainyun -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `RAINYUN_API_KEY` | API key | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `RAINYUN_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `RAINYUN_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `RAINYUN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `RAINYUN_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://www.apifox.cn/apidoc/shared-a4595cc8-44c5-4678-a2a3-eed7738dab03/api-151416609) - - - - diff --git a/docs/content/dns/zz_gen_rcodezero.md b/docs/content/dns/zz_gen_rcodezero.md index a544df420..bca7dcbd3 100644 --- a/docs/content/dns/zz_gen_rcodezero.md +++ b/docs/content/dns/zz_gen_rcodezero.md @@ -27,7 +27,7 @@ Here is an example bash command using the RcodeZero provider: ```bash RCODEZERO_API_TOKEN= \ -lego --dns rcodezero -d '*.example.com' -d example.com run +lego --email you@example.com --dns rcodezero --domains my.example.org run ``` @@ -40,20 +40,20 @@ lego --dns rcodezero -d '*.example.com' -d example.com run | `RCODEZERO_API_TOKEN` | API token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `RCODEZERO_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `RCODEZERO_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | -| `RCODEZERO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 240) | -| `RCODEZERO_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `RCODEZERO_HTTP_TIMEOUT` | API request timeout | +| `RCODEZERO_POLLING_INTERVAL` | Time between DNS propagation check | +| `RCODEZERO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `RCODEZERO_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Description diff --git a/docs/content/dns/zz_gen_regfish.md b/docs/content/dns/zz_gen_regfish.md deleted file mode 100644 index 357ce0764..000000000 --- a/docs/content/dns/zz_gen_regfish.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: "Regfish" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: regfish -dnsprovider: - since: "v4.20.0" - code: "regfish" - url: "https://regfish.de/" ---- - - - - - - -Configuration for [Regfish](https://regfish.de/). - - - - -- Code: `regfish` -- Since: v4.20.0 - - -Here is an example bash command using the Regfish provider: - -```bash -REGFISH_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns regfish -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `REGFISH_API_KEY` | API key | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `REGFISH_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `REGFISH_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `REGFISH_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `REGFISH_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://regfish.readme.io/) -- [Go client](https://github.com/regfish/regfish-dnsapi-go) - - - - diff --git a/docs/content/dns/zz_gen_regru.md b/docs/content/dns/zz_gen_regru.md index eaf163a13..c724cae98 100644 --- a/docs/content/dns/zz_gen_regru.md +++ b/docs/content/dns/zz_gen_regru.md @@ -28,7 +28,7 @@ Here is an example bash command using the reg.ru provider: ```bash REGRU_USERNAME=xxxxxx \ REGRU_PASSWORD=yyyyyy \ -lego --dns regru -d '*.example.com' -d example.com run +lego --email you@example.com --dns regru --domains my.example.org run ``` @@ -42,22 +42,22 @@ lego --dns regru -d '*.example.com' -d example.com run | `REGRU_USERNAME` | API username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `REGRU_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `REGRU_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `REGRU_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `REGRU_HTTP_TIMEOUT` | API request timeout | +| `REGRU_POLLING_INTERVAL` | Time between DNS propagation check | +| `REGRU_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `REGRU_TLS_CERT` | authentication certificate | | `REGRU_TLS_KEY` | authentication private key | -| `REGRU_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `REGRU_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_rfc2136.md b/docs/content/dns/zz_gen_rfc2136.md index 1b1d43dd5..d84fa4ead 100644 --- a/docs/content/dns/zz_gen_rfc2136.md +++ b/docs/content/dns/zz_gen_rfc2136.md @@ -27,18 +27,20 @@ Here is an example bash command using the RFC2136 provider: ```bash RFC2136_NAMESERVER=127.0.0.1 \ -RFC2136_TSIG_KEY=example.com \ +RFC2136_TSIG_KEY=lego \ RFC2136_TSIG_ALGORITHM=hmac-sha256. \ RFC2136_TSIG_SECRET=YWJjZGVmZGdoaWprbG1ub3BxcnN0dXZ3eHl6MTIzNDU= \ -lego --dns rfc2136 -d '*.example.com' -d example.com run +lego --email you@example.com --dns rfc2136 --domains my.example.org run ## --- -keyname=example.com; keyfile=example.com.key; tsig-keygen $keyname > $keyfile +keyname=lego; keyfile=lego.key; tsig-keygen $keyname > $keyfile RFC2136_NAMESERVER=127.0.0.1 \ -RFC2136_TSIG_FILE="$keyfile" \ -lego --dns rfc2136 -d '*.example.com' -d example.com run +RFC2136_TSIG_KEY="$keyname" \ +RFC2136_TSIG_ALGORITHM="$( awk -F'[ ";]' '/algorithm/ { print $2 }' $keyfile )." \ +RFC2136_TSIG_SECRET="$( awk -F'[ ";]' '/secret/ { print $3 }' $keyfile )" \ +lego --email you@example.com --dns rfc2136 --domains my.example.org run ``` @@ -49,27 +51,26 @@ lego --dns rfc2136 -d '*.example.com' -d example.com run | Environment Variable Name | Description | |-----------------------|-------------| | `RFC2136_NAMESERVER` | Network address in the form "host" or "host:port" | -| `RFC2136_TSIG_ALGORITHM` | TSIG algorithm. See [miekg/dns#tsig.go](https://github.com/miekg/dns/blob/master/tsig.go) for supported values. To disable TSIG authentication, leave the `RFC2136_TSIG_KEY` or `RFC2136_TSIG_SECRET` variables unset. | -| `RFC2136_TSIG_KEY` | Name of the secret key as defined in DNS server configuration. To disable TSIG authentication, leave the `RFC2136_TSIG_KEY` variable unset. | -| `RFC2136_TSIG_SECRET` | Secret key payload. To disable TSIG authentication, leave the `RFC2136_TSIG_SECRET` variable unset. | +| `RFC2136_TSIG_ALGORITHM` | TSIG algorithm. See [miekg/dns#tsig.go](https://github.com/miekg/dns/blob/master/tsig.go) for supported values. To disable TSIG authentication, leave the `RFC2136_TSIG*` variables unset. | +| `RFC2136_TSIG_KEY` | Name of the secret key as defined in DNS server configuration. To disable TSIG authentication, leave the `RFC2136_TSIG*` variables unset. | +| `RFC2136_TSIG_SECRET` | Secret key payload. To disable TSIG authentication, leave the` RFC2136_TSIG*` variables unset. | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `RFC2136_DNS_TIMEOUT` | API request timeout in seconds (Default: 10) | -| `RFC2136_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `RFC2136_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `RFC2136_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) | -| `RFC2136_TSIG_FILE` | Path to a key file generated by tsig-keygen | -| `RFC2136_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `RFC2136_DNS_TIMEOUT` | API request timeout | +| `RFC2136_POLLING_INTERVAL` | Time between DNS propagation check | +| `RFC2136_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `RFC2136_SEQUENCE_INTERVAL` | Time between sequential requests | +| `RFC2136_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_rimuhosting.md b/docs/content/dns/zz_gen_rimuhosting.md index acb829e93..a28a5683b 100644 --- a/docs/content/dns/zz_gen_rimuhosting.md +++ b/docs/content/dns/zz_gen_rimuhosting.md @@ -27,7 +27,7 @@ Here is an example bash command using the RimuHosting provider: ```bash RIMUHOSTING_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --dns rimuhosting -d '*.example.com' -d example.com run +lego --email you@example.com --dns rimuhosting --domains my.example.org run ``` @@ -40,20 +40,20 @@ lego --dns rimuhosting -d '*.example.com' -d example.com run | `RIMUHOSTING_API_KEY` | User API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `RIMUHOSTING_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `RIMUHOSTING_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `RIMUHOSTING_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `RIMUHOSTING_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) | +| `RIMUHOSTING_HTTP_TIMEOUT` | API request timeout | +| `RIMUHOSTING_POLLING_INTERVAL` | Time between DNS propagation check | +| `RIMUHOSTING_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `RIMUHOSTING_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_route53.md b/docs/content/dns/zz_gen_route53.md index 59e489d6a..62ef172af 100644 --- a/docs/content/dns/zz_gen_route53.md +++ b/docs/content/dns/zz_gen_route53.md @@ -30,7 +30,7 @@ AWS_ACCESS_KEY_ID=your_key_id \ AWS_SECRET_ACCESS_KEY=your_secret_access_key \ AWS_REGION=aws-region \ AWS_HOSTED_ZONE_ID=your_hosted_zone_id \ -lego --dns route53 -d '*.example.com' -d example.com run +lego --domains example.com --email your_example@email.com --dns route53 --accept-tos=true run ``` @@ -51,7 +51,7 @@ lego --dns route53 -d '*.example.com' -d example.com run | `AWS_WAIT_FOR_RECORD_SETS_CHANGED` | Wait for changes to be INSYNC (it can be unstable) | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -59,14 +59,13 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | Environment Variable Name | Description | |--------------------------------|-------------| | `AWS_MAX_RETRIES` | The number of maximum returns the service will use to make an individual API request | -| `AWS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 4) | -| `AWS_PRIVATE_ZONE` | Set to true to use private zones only (default: use public zones only) | -| `AWS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | +| `AWS_POLLING_INTERVAL` | Time between DNS propagation check | +| `AWS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `AWS_SHARED_CREDENTIALS_FILE` | Managed by the AWS client. Shared credentials file. | -| `AWS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 10) | +| `AWS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Description diff --git a/docs/content/dns/zz_gen_safedns.md b/docs/content/dns/zz_gen_safedns.md index 4c20fca6a..b07b0591e 100644 --- a/docs/content/dns/zz_gen_safedns.md +++ b/docs/content/dns/zz_gen_safedns.md @@ -1,12 +1,12 @@ --- -title: "ANS SafeDNS" +title: "UKFast SafeDNS" date: 2019-03-03T16:39:46+01:00 draft: false slug: safedns dnsprovider: since: "v4.6.0" code: "safedns" - url: "https://www.ans.co.uk/" + url: "https://www.ukfast.co.uk/dns-hosting.html" --- @@ -14,7 +14,7 @@ dnsprovider: -Configuration for [ANS SafeDNS](https://www.ans.co.uk/). +Configuration for [UKFast SafeDNS](https://www.ukfast.co.uk/dns-hosting.html). @@ -23,11 +23,11 @@ Configuration for [ANS SafeDNS](https://www.ans.co.uk/). - Since: v4.6.0 -Here is an example bash command using the ANS SafeDNS provider: +Here is an example bash command using the UKFast SafeDNS provider: ```bash SAFEDNS_AUTH_TOKEN=xxxxxx \ -lego --dns safedns -d '*.example.com' -d example.com run +lego --email you@example.com --dns safedns --domains my.example.org run ``` @@ -40,20 +40,20 @@ lego --dns safedns -d '*.example.com' -d example.com run | `SAFEDNS_AUTH_TOKEN` | Authentication token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `SAFEDNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `SAFEDNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `SAFEDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `SAFEDNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `SAFEDNS_HTTP_TIMEOUT` | API request timeout | +| `SAFEDNS_POLLING_INTERVAL` | Time between DNS propagation check | +| `SAFEDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `SAFEDNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_sakuracloud.md b/docs/content/dns/zz_gen_sakuracloud.md index b43f83ef4..628294d83 100644 --- a/docs/content/dns/zz_gen_sakuracloud.md +++ b/docs/content/dns/zz_gen_sakuracloud.md @@ -28,7 +28,7 @@ Here is an example bash command using the Sakura Cloud provider: ```bash SAKURACLOUD_ACCESS_TOKEN=xxxxx \ SAKURACLOUD_ACCESS_TOKEN_SECRET=yyyyy \ -lego --dns sakuracloud -d '*.example.com' -d example.com run +lego --email you@example.com --dns sakuracloud --domains my.example.org run ``` @@ -42,20 +42,20 @@ lego --dns sakuracloud -d '*.example.com' -d example.com run | `SAKURACLOUD_ACCESS_TOKEN_SECRET` | Access token secret | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `SAKURACLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | -| `SAKURACLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `SAKURACLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `SAKURACLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `SAKURACLOUD_HTTP_TIMEOUT` | API request timeout | +| `SAKURACLOUD_POLLING_INTERVAL` | Time between DNS propagation check | +| `SAKURACLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `SAKURACLOUD_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_scaleway.md b/docs/content/dns/zz_gen_scaleway.md index 4033a9bd6..78ff0d7e8 100644 --- a/docs/content/dns/zz_gen_scaleway.md +++ b/docs/content/dns/zz_gen_scaleway.md @@ -27,7 +27,7 @@ Here is an example bash command using the Scaleway provider: ```bash SCW_SECRET_KEY=xxxxxxx-xxxxx-xxxx-xxx-xxxxxx \ -lego --dns scaleway -d '*.example.com' -d example.com run +lego --email you@example.com --dns scaleway --domains my.example.org run ``` @@ -41,7 +41,7 @@ lego --dns scaleway -d '*.example.com' -d example.com run | `SCW_SECRET_KEY` | Secret key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -49,13 +49,12 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | Environment Variable Name | Description | |--------------------------------|-------------| | `SCW_ACCESS_KEY` | Access key | -| `SCW_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `SCW_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | -| `SCW_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `SCW_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | +| `SCW_POLLING_INTERVAL` | Time between DNS propagation check | +| `SCW_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `SCW_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_selectel.md b/docs/content/dns/zz_gen_selectel.md index d994d6633..7fbce1110 100644 --- a/docs/content/dns/zz_gen_selectel.md +++ b/docs/content/dns/zz_gen_selectel.md @@ -27,7 +27,7 @@ Here is an example bash command using the Selectel provider: ```bash SELECTEL_API_TOKEN=xxxxx \ -lego --dns selectel -d '*.example.com' -d example.com run +lego --email you@example.com --dns selectel --domains my.example.org run ``` @@ -40,7 +40,7 @@ lego --dns selectel -d '*.example.com' -d example.com run | `SELECTEL_API_TOKEN` | API token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -48,13 +48,13 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | Environment Variable Name | Description | |--------------------------------|-------------| | `SELECTEL_BASE_URL` | API endpoint URL | -| `SELECTEL_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `SELECTEL_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `SELECTEL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `SELECTEL_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | +| `SELECTEL_HTTP_TIMEOUT` | API request timeout | +| `SELECTEL_POLLING_INTERVAL` | Time between DNS propagation check | +| `SELECTEL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `SELECTEL_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_selectelv2.md b/docs/content/dns/zz_gen_selectelv2.md index 0873d810c..8676ef39a 100644 --- a/docs/content/dns/zz_gen_selectelv2.md +++ b/docs/content/dns/zz_gen_selectelv2.md @@ -26,11 +26,11 @@ Configuration for [Selectel v2](https://selectel.ru). Here is an example bash command using the Selectel v2 provider: ```bash -SELECTELV2_USERNAME=trex \ -SELECTELV2_PASSWORD=xxxxx \ -SELECTELV2_ACCOUNT_ID=1234567 \ -SELECTELV2_PROJECT_ID=111a11111aaa11aa1a11aaa11111aa1a \ -lego --dns selectelv2 -d '*.example.com' -d example.com run +SELECTEL_USERNAME=trex \ +SELECTEL_PASSWORD=xxxxx \ +SELECTEL_ACCOUNT_ID=1234567 \ +SELECTEL_PROJECT_ID=111a11111aaa11aa1a11aaa11111aa1a \ +lego --email you@example.com --dns selectelv2 --domains my.example.org run ``` @@ -46,24 +46,21 @@ lego --dns selectelv2 -d '*.example.com' -d example.com run | `SELECTELV2_USERNAME` | Openstack username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `SELECTELV2_AUTH_REGION` | Location for auth endpoint like ResellAPI or Keystone (default: 'ru-1') | -| `SELECTELV2_AUTH_URL` | Identity endpoint (defaul: 'https://cloud.api.selcloud.ru/identity/v3/') | | `SELECTELV2_BASE_URL` | API endpoint URL | -| `SELECTELV2_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `SELECTELV2_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 5) | -| `SELECTELV2_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `SELECTELV2_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | -| `SELECTELV2_USER_DOMAIN_NAME` | To specify the domain name (account ID) where the user is located. (default: SELECTELV2_ACCOUNT_ID) | +| `SELECTELV2_HTTP_TIMEOUT` | API request timeout | +| `SELECTELV2_POLLING_INTERVAL` | Time between DNS propagation check | +| `SELECTELV2_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `SELECTELV2_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_selfhostde.md b/docs/content/dns/zz_gen_selfhostde.md deleted file mode 100644 index 363f782e0..000000000 --- a/docs/content/dns/zz_gen_selfhostde.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: "SelfHost.(de|eu)" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: selfhostde -dnsprovider: - since: "v4.19.0" - code: "selfhostde" - url: "https://www.selfhost.de" ---- - - - - - - -Configuration for [SelfHost.(de|eu)](https://www.selfhost.de). - - - - -- Code: `selfhostde` -- Since: v4.19.0 - - -Here is an example bash command using the SelfHost.(de|eu) provider: - -```bash -SELFHOSTDE_USERNAME=xxx \ -SELFHOSTDE_PASSWORD=yyy \ -SELFHOSTDE_RECORDS_MAPPING=my.example.com:123 \ -lego --dns selfhostde -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `SELFHOSTDE_PASSWORD` | Password | -| `SELFHOSTDE_RECORDS_MAPPING` | Record IDs mapping with domains (ex: example.com:123:456,example.org:789,foo.example.com:147) | -| `SELFHOSTDE_USERNAME` | Username | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `SELFHOSTDE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `SELFHOSTDE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 30) | -| `SELFHOSTDE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 240) | -| `SELFHOSTDE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - -SelfHost.de doesn't have an API to create or delete TXT records, -there is only an "unofficial" and undocumented endpoint to update an existing TXT record. - -So, before using lego to request a certificate for a given domain or wildcard (such as `my.example.org` or `*.my.example.org`), -you must create: - -- one TXT record named `_acme-challenge.my.example.org` if you are **not** using wildcard for this domain. -- two TXT records named `_acme-challenge.my.example.org` if you are using wildcard for this domain. - -After that you must edit the TXT record(s) to get the ID(s). - -You then must prepare the `SELFHOSTDE_RECORDS_MAPPING` environment variable with the following format: - -``` -::,::,:: -``` - -where each group of domain + record ID(s) is separated with a comma (`,`), -and the domain and record ID(s) are separated with a colon (`:`). - -For example, if you want to create or renew a certificate for `my.example.org`, `*.my.example.org`, and `other.example.org`, -you would need: - -- two separate records for `_acme-challenge.my.example.org` -- and another separate record for `_acme-challenge.other.example.org` - -The resulting environment variable would then be: `SELFHOSTDE_RECORDS_MAPPING=my.example.com:123:456,other.example.com:789` - - - - - - - - diff --git a/docs/content/dns/zz_gen_servercow.md b/docs/content/dns/zz_gen_servercow.md index 7d00a6306..86f631680 100644 --- a/docs/content/dns/zz_gen_servercow.md +++ b/docs/content/dns/zz_gen_servercow.md @@ -28,7 +28,7 @@ Here is an example bash command using the Servercow provider: ```bash SERVERCOW_USERNAME=xxxxxxxx \ SERVERCOW_PASSWORD=xxxxxxxx \ -lego --dns servercow -d '*.example.com' -d example.com run +lego --email you@example.com --dns servercow --domains my.example.org run ``` @@ -42,27 +42,27 @@ lego --dns servercow -d '*.example.com' -d example.com run | `SERVERCOW_USERNAME` | API username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `SERVERCOW_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `SERVERCOW_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `SERVERCOW_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `SERVERCOW_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `SERVERCOW_HTTP_TIMEOUT` | API request timeout | +| `SERVERCOW_POLLING_INTERVAL` | Time between DNS propagation check | +| `SERVERCOW_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `SERVERCOW_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information -- [API documentation](https://wiki.servercow.de/en/domains/dns_api/api-syntax/) +- [API documentation](https://cp.servercow.de/client/plugin/support_manager/knowledgebase/view/34/dns-api-v1/7/) diff --git a/docs/content/dns/zz_gen_shellrent.md b/docs/content/dns/zz_gen_shellrent.md index cbbc172e2..4171e1248 100644 --- a/docs/content/dns/zz_gen_shellrent.md +++ b/docs/content/dns/zz_gen_shellrent.md @@ -28,7 +28,7 @@ Here is an example bash command using the Shellrent provider: ```bash SHELLRENT_USERNAME=xxxx \ SHELLRENT_TOKEN=yyyy \ -lego --dns shellrent -d '*.example.com' -d example.com run +lego --email you@example.com --dns shellrent --domains my.example.org run ``` @@ -42,20 +42,20 @@ lego --dns shellrent -d '*.example.com' -d example.com run | `SHELLRENT_USERNAME` | Username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `SHELLRENT_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `SHELLRENT_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | -| `SHELLRENT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) | -| `SHELLRENT_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) | +| `SHELLRENT_HTTP_TIMEOUT` | API request timeout | +| `SHELLRENT_POLLING_INTERVAL` | Time between DNS propagation check | +| `SHELLRENT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `SHELLRENT_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_simply.md b/docs/content/dns/zz_gen_simply.md index edfa14380..bce1410a1 100644 --- a/docs/content/dns/zz_gen_simply.md +++ b/docs/content/dns/zz_gen_simply.md @@ -28,7 +28,7 @@ Here is an example bash command using the Simply.com provider: ```bash SIMPLY_ACCOUNT_NAME=xxxxxx \ SIMPLY_API_KEY=yyyyyy \ -lego --dns simply -d '*.example.com' -d example.com run +lego --email you@example.com --dns simply --domains my.example.org run ``` @@ -42,20 +42,20 @@ lego --dns simply -d '*.example.com' -d example.com run | `SIMPLY_API_KEY` | API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `SIMPLY_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `SIMPLY_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | -| `SIMPLY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) | -| `SIMPLY_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `SIMPLY_HTTP_TIMEOUT` | API request timeout | +| `SIMPLY_POLLING_INTERVAL` | Time between DNS propagation check | +| `SIMPLY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `SIMPLY_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_sonic.md b/docs/content/dns/zz_gen_sonic.md index 20729bc1a..bd7674a73 100644 --- a/docs/content/dns/zz_gen_sonic.md +++ b/docs/content/dns/zz_gen_sonic.md @@ -28,7 +28,7 @@ Here is an example bash command using the Sonic provider: ```bash SONIC_USER_ID=12345 \ SONIC_API_KEY=4d6fbf2f9ab0fa11697470918d37625851fc0c51 \ -lego --dns sonic -d '*.example.com' -d example.com run +lego --email you@example.com --dns sonic --domains my.example.org run ``` @@ -42,21 +42,21 @@ lego --dns sonic -d '*.example.com' -d example.com run | `SONIC_USER_ID` | User ID | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `SONIC_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | -| `SONIC_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `SONIC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `SONIC_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) | -| `SONIC_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `SONIC_HTTP_TIMEOUT` | API request timeout | +| `SONIC_POLLING_INTERVAL` | Time between DNS propagation check | +| `SONIC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `SONIC_SEQUENCE_INTERVAL` | Time between sequential requests | +| `SONIC_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## API keys diff --git a/docs/content/dns/zz_gen_spaceship.md b/docs/content/dns/zz_gen_spaceship.md deleted file mode 100644 index 9f3b51e43..000000000 --- a/docs/content/dns/zz_gen_spaceship.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: "Spaceship" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: spaceship -dnsprovider: - since: "v4.22.0" - code: "spaceship" - url: "https://www.spaceship.com/" ---- - - - - - - -Configuration for [Spaceship](https://www.spaceship.com/). - - - - -- Code: `spaceship` -- Since: v4.22.0 - - -Here is an example bash command using the Spaceship provider: - -```bash -SPACESHIP_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -SPACESHIP_API_SECRET="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns spaceship -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `SPACESHIP_API_KEY` | API key | -| `SPACESHIP_API_SECRET` | API secret | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `SPACESHIP_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `SPACESHIP_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `SPACESHIP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `SPACESHIP_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://docs.spaceship.dev/#tag/DNS-records) - - - - diff --git a/docs/content/dns/zz_gen_stackpath.md b/docs/content/dns/zz_gen_stackpath.md index b881176f4..d07b30404 100644 --- a/docs/content/dns/zz_gen_stackpath.md +++ b/docs/content/dns/zz_gen_stackpath.md @@ -29,7 +29,7 @@ Here is an example bash command using the Stackpath provider: STACKPATH_CLIENT_ID=xxxxx \ STACKPATH_CLIENT_SECRET=yyyyy \ STACKPATH_STACK_ID=zzzzz \ -lego --dns stackpath -d '*.example.com' -d example.com run +lego --email you@example.com --dns stackpath --domains my.example.org run ``` @@ -44,19 +44,19 @@ lego --dns stackpath -d '*.example.com' -d example.com run | `STACKPATH_STACK_ID` | Stack ID | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `STACKPATH_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `STACKPATH_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `STACKPATH_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `STACKPATH_POLLING_INTERVAL` | Time between DNS propagation check | +| `STACKPATH_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `STACKPATH_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_syse.md b/docs/content/dns/zz_gen_syse.md deleted file mode 100644 index a1a952bc5..000000000 --- a/docs/content/dns/zz_gen_syse.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: "Syse" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: syse -dnsprovider: - since: "v4.30.0" - code: "syse" - url: "https://www.syse.no/" ---- - - - - - - -Configuration for [Syse](https://www.syse.no/). - - - - -- Code: `syse` -- Since: v4.30.0 - - -Here is an example bash command using the Syse provider: - -```bash -SYSE_CREDENTIALS=example.com:password \ -lego --dns syse -d '*.example.com' -d example.com run - -SYSE_CREDENTIALS=example.org:password1,example.com:password2 \ -lego --dns syse -d '*.example.org' -d example.org -d '*.example.com' -d example.com -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `SYSE_CREDENTIALS` | Comma-separated list of `zone:password` credential pairs | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `SYSE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `SYSE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | -| `SYSE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 1200) | -| `SYSE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://www.syse.no/api/dns) - - - - diff --git a/docs/content/dns/zz_gen_technitium.md b/docs/content/dns/zz_gen_technitium.md deleted file mode 100644 index ff7f2e6ed..000000000 --- a/docs/content/dns/zz_gen_technitium.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: "Technitium" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: technitium -dnsprovider: - since: "v4.20.0" - code: "technitium" - url: "https://technitium.com/" ---- - - - - - - -Configuration for [Technitium](https://technitium.com/). - - - - -- Code: `technitium` -- Since: v4.20.0 - - -Here is an example bash command using the Technitium provider: - -```bash -TECHNITIUM_SERVER_BASE_URL="https://localhost:5380" \ -TECHNITIUM_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns technitium -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `TECHNITIUM_API_TOKEN` | API token | -| `TECHNITIUM_SERVER_BASE_URL` | Server base URL | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `TECHNITIUM_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `TECHNITIUM_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `TECHNITIUM_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `TECHNITIUM_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - -Technitium DNS Server supports Dynamic Updates (RFC2136) for primary zones, -so you can also use the [RFC2136 provider](https://go-acme.github.io/lego/dns/rfc2136/index.html). - -[RFC2136 provider](https://go-acme.github.io/lego/dns/rfc2136/index.html) is much better compared to the HTTP API option from security perspective. -Technitium recommends to use it in production over the HTTP API. - - - -## More information - -- [API documentation](https://github.com/TechnitiumSoftware/DnsServer/blob/0f83d23e605956b66ac76921199e241d9cc061bd/APIDOCS.md) - - - - diff --git a/docs/content/dns/zz_gen_tencentcloud.md b/docs/content/dns/zz_gen_tencentcloud.md index 178ffcf43..993100a63 100644 --- a/docs/content/dns/zz_gen_tencentcloud.md +++ b/docs/content/dns/zz_gen_tencentcloud.md @@ -6,7 +6,7 @@ slug: tencentcloud dnsprovider: since: "v4.6.0" code: "tencentcloud" - url: "https://cloud.tencent.com/product/dns" + url: "https://cloud.tencent.com/product/cns" --- @@ -14,7 +14,7 @@ dnsprovider: -Configuration for [Tencent Cloud DNS](https://cloud.tencent.com/product/dns). +Configuration for [Tencent Cloud DNS](https://cloud.tencent.com/product/cns). @@ -28,7 +28,7 @@ Here is an example bash command using the Tencent Cloud DNS provider: ```bash TENCENTCLOUD_SECRET_ID=abcdefghijklmnopqrstuvwx \ TENCENTCLOUD_SECRET_KEY=your-secret-key \ -lego --dns tencentcloud -d '*.example.com' -d example.com run +lego --email you@example.com --dns tencentcloud --domains my.example.org run ``` @@ -42,22 +42,22 @@ lego --dns tencentcloud -d '*.example.com' -d example.com run | `TENCENTCLOUD_SECRET_KEY` | Access Key secret | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `TENCENTCLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `TENCENTCLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `TENCENTCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `TENCENTCLOUD_HTTP_TIMEOUT` | API request timeout | +| `TENCENTCLOUD_POLLING_INTERVAL` | Time between DNS propagation check | +| `TENCENTCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `TENCENTCLOUD_REGION` | Region | | `TENCENTCLOUD_SESSION_TOKEN` | Access Key token | -| `TENCENTCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) | +| `TENCENTCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_timewebcloud.md b/docs/content/dns/zz_gen_timewebcloud.md deleted file mode 100644 index 83d5b831b..000000000 --- a/docs/content/dns/zz_gen_timewebcloud.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: "Timeweb Cloud" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: timewebcloud -dnsprovider: - since: "v4.20.0" - code: "timewebcloud" - url: "https://timeweb.cloud/" ---- - - - - - - -Configuration for [Timeweb Cloud](https://timeweb.cloud/). - - - - -- Code: `timewebcloud` -- Since: v4.20.0 - - -Here is an example bash command using the Timeweb Cloud provider: - -```bash -TIMEWEBCLOUD_AUTH_TOKEN=xxxxxx \ -lego --dns timewebcloud -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `TIMEWEBCLOUD_AUTH_TOKEN` | Authentication token | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `TIMEWEBCLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | -| `TIMEWEBCLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `TIMEWEBCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://timeweb.cloud/api-docs) - - - - diff --git a/docs/content/dns/zz_gen_todaynic.md b/docs/content/dns/zz_gen_todaynic.md deleted file mode 100644 index 7b06c012d..000000000 --- a/docs/content/dns/zz_gen_todaynic.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: "TodayNIC/时代互联" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: todaynic -dnsprovider: - since: "v4.32.0" - code: "todaynic" - url: "https://www.todaynic.com/" ---- - - - - - - -Configuration for [TodayNIC/时代互联](https://www.todaynic.com/). - - - - -- Code: `todaynic` -- Since: v4.32.0 - - -Here is an example bash command using the TodayNIC/时代互联 provider: - -```bash -TODAYNIC_AUTH_USER_ID="xxx" \ -TODAYNIC_API_KEY="yyy" \ -lego --dns todaynic -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `TODAYNIC_API_KEY` | API key | -| `TODAYNIC_AUTH_USER_ID` | account ID | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `TODAYNIC_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `TODAYNIC_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `TODAYNIC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `TODAYNIC_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://www.todaynic.com/partner/mode_Http_Api_detail.php) - - - - diff --git a/docs/content/dns/zz_gen_transip.md b/docs/content/dns/zz_gen_transip.md index a66a25879..a4c196346 100644 --- a/docs/content/dns/zz_gen_transip.md +++ b/docs/content/dns/zz_gen_transip.md @@ -28,7 +28,7 @@ Here is an example bash command using the TransIP provider: ```bash TRANSIP_ACCOUNT_NAME = "Account name" \ TRANSIP_PRIVATE_KEY_PATH = "transip.key" \ -lego --dns transip -d '*.example.com' -d example.com run +lego --email you@example.com --dns transip --domains my.example.org run ``` @@ -42,20 +42,19 @@ lego --dns transip -d '*.example.com' -d example.com run | `TRANSIP_PRIVATE_KEY_PATH` | Private key path | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `TRANSIP_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `TRANSIP_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | -| `TRANSIP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 600) | -| `TRANSIP_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 10) | +| `TRANSIP_POLLING_INTERVAL` | Time between DNS propagation check | +| `TRANSIP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `TRANSIP_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_ultradns.md b/docs/content/dns/zz_gen_ultradns.md index d6d89c77b..f0a4d313c 100644 --- a/docs/content/dns/zz_gen_ultradns.md +++ b/docs/content/dns/zz_gen_ultradns.md @@ -28,7 +28,7 @@ Here is an example bash command using the Ultradns provider: ```bash ULTRADNS_USERNAME=username \ ULTRADNS_PASSWORD=password \ -lego --dns ultradns -d '*.example.com' -d example.com run +lego --email you@example.com --dns ultradns --domains my.example.org run ``` @@ -42,7 +42,7 @@ lego --dns ultradns -d '*.example.com' -d example.com run | `ULTRADNS_USERNAME` | API Username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -50,12 +50,12 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | Environment Variable Name | Description | |--------------------------------|-------------| | `ULTRADNS_ENDPOINT` | API endpoint URL, defaults to https://api.ultradns.com/ | -| `ULTRADNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 4) | -| `ULTRADNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `ULTRADNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `ULTRADNS_POLLING_INTERVAL` | Time between DNS propagation check | +| `ULTRADNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `ULTRADNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_uniteddomains.md b/docs/content/dns/zz_gen_uniteddomains.md deleted file mode 100644 index e837644d5..000000000 --- a/docs/content/dns/zz_gen_uniteddomains.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: "United-Domains" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: uniteddomains -dnsprovider: - since: "v4.29.0" - code: "uniteddomains" - url: "https://www.united-domains.de/" ---- - - - - - - -Configuration for [United-Domains](https://www.united-domains.de/). - - - - -- Code: `uniteddomains` -- Since: v4.29.0 - - -Here is an example bash command using the United-Domains provider: - -```bash -UNITEDDOMAINS_API_KEY=xxxxxxxx \ -lego --dns uniteddomains -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `UNITEDDOMAINS_API_KEY` | API key `.` https://www.united-domains.de/help/faq-article/getting-started-with-the-united-domains-dns-api/ | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `UNITEDDOMAINS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `UNITEDDOMAINS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `UNITEDDOMAINS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 900) | -| `UNITEDDOMAINS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://www.united-domains.de/dns-apidoc/) - - - - diff --git a/docs/content/dns/zz_gen_variomedia.md b/docs/content/dns/zz_gen_variomedia.md index f9771c867..a34d857fd 100644 --- a/docs/content/dns/zz_gen_variomedia.md +++ b/docs/content/dns/zz_gen_variomedia.md @@ -27,7 +27,7 @@ Here is an example bash command using the Variomedia provider: ```bash VARIOMEDIA_API_TOKEN=xxxx \ -lego --dns variomedia -d '*.example.com' -d example.com run +lego --email you@example.com --dns variomedia --domains my.example.org run ``` @@ -40,21 +40,21 @@ lego --dns variomedia -d '*.example.com' -d example.com run | `VARIOMEDIA_API_TOKEN` | API token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `VARIOMEDIA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `VARIOMEDIA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `VARIOMEDIA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `VARIOMEDIA_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) | -| `VARIOMEDIA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `DODE_SEQUENCE_INTERVAL` | Time between sequential requests | +| `VARIOMEDIA_HTTP_TIMEOUT` | API request timeout | +| `VARIOMEDIA_POLLING_INTERVAL` | Time between DNS propagation check | +| `VARIOMEDIA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `VARIOMEDIA_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_vegadns.md b/docs/content/dns/zz_gen_vegadns.md index e06eebce7..0dafc7ef6 100644 --- a/docs/content/dns/zz_gen_vegadns.md +++ b/docs/content/dns/zz_gen_vegadns.md @@ -39,19 +39,19 @@ _Please contribute by adding a CLI example._ | `VEGADNS_URL` | API endpoint URL | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `VEGADNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 60) | -| `VEGADNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 720) | -| `VEGADNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 10) | +| `VEGADNS_POLLING_INTERVAL` | Time between DNS propagation check | +| `VEGADNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `VEGADNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_vercel.md b/docs/content/dns/zz_gen_vercel.md index 71f2eeed5..817f4764b 100644 --- a/docs/content/dns/zz_gen_vercel.md +++ b/docs/content/dns/zz_gen_vercel.md @@ -27,7 +27,7 @@ Here is an example bash command using the Vercel provider: ```bash VERCEL_API_TOKEN=xxxxxx \ -lego --dns vercel -d '*.example.com' -d example.com run +lego --email you@example.com --dns vercel --domains my.example.org run ``` @@ -40,21 +40,21 @@ lego --dns vercel -d '*.example.com' -d example.com run | `VERCEL_API_TOKEN` | Authentication token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `VERCEL_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `VERCEL_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 5) | -| `VERCEL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `VERCEL_HTTP_TIMEOUT` | API request timeout | +| `VERCEL_POLLING_INTERVAL` | Time between DNS propagation check | +| `VERCEL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `VERCEL_TEAM_ID` | Team ID (ex: team_xxxxxxxxxxxxxxxxxxxxxxxx) | -| `VERCEL_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | +| `VERCEL_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_versio.md b/docs/content/dns/zz_gen_versio.md index 5d2cc0118..f6f987738 100644 --- a/docs/content/dns/zz_gen_versio.md +++ b/docs/content/dns/zz_gen_versio.md @@ -28,7 +28,7 @@ Here is an example bash command using the Versio.[nl|eu|uk] provider: ```bash VERSIO_USERNAME= \ VERSIO_PASSWORD= \ -lego --dns versio -d '*.example.com' -d example.com run +lego --email you@example.com --dns versio --domains my.example.org run ``` @@ -42,7 +42,7 @@ lego --dns versio -d '*.example.com' -d example.com run | `VERSIO_USERNAME` | Basic authentication username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -50,14 +50,14 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | Environment Variable Name | Description | |--------------------------------|-------------| | `VERSIO_ENDPOINT` | The endpoint URL of the API Server | -| `VERSIO_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `VERSIO_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 5) | -| `VERSIO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `VERSIO_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) | -| `VERSIO_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `VERSIO_HTTP_TIMEOUT` | API request timeout | +| `VERSIO_POLLING_INTERVAL` | Time between DNS propagation check | +| `VERSIO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `VERSIO_SEQUENCE_INTERVAL` | Time between sequential requests, default 60s | +| `VERSIO_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). To test with the sandbox environment set ```VERSIO_ENDPOINT=https://www.versio.nl/testapi/v1/``` diff --git a/docs/content/dns/zz_gen_vinyldns.md b/docs/content/dns/zz_gen_vinyldns.md index 3280d6f0a..5007f9636 100644 --- a/docs/content/dns/zz_gen_vinyldns.md +++ b/docs/content/dns/zz_gen_vinyldns.md @@ -29,7 +29,7 @@ Here is an example bash command using the VinylDNS provider: VINYLDNS_ACCESS_KEY=xxxxxx \ VINYLDNS_SECRET_KEY=yyyyy \ VINYLDNS_HOST=https://api.vinyldns.example.org:9443 \ -lego --dns vinyldns -d '*.example.com' -d example.com run +lego --email you@example.com --dns vinyldns --domains my.example.org run ``` @@ -44,21 +44,19 @@ lego --dns vinyldns -d '*.example.com' -d example.com run | `VINYLDNS_SECRET_KEY` | The VinylDNS API Secret key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `VINYLDNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `VINYLDNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 4) | -| `VINYLDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `VINYLDNS_QUOTE_VALUE` | Adds quotes around the TXT record value (Default: false) | -| `VINYLDNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 30) | +| `VINYLDNS_POLLING_INTERVAL` | Time between DNS propagation check | +| `VINYLDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `VINYLDNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). The vinyldns integration makes use of dotted hostnames to ease permission management. Users are required to have DELETE ACL level or zone admin permissions on the VinylDNS zone containing the target host. diff --git a/docs/content/dns/zz_gen_virtualname.md b/docs/content/dns/zz_gen_virtualname.md deleted file mode 100644 index a00e5105f..000000000 --- a/docs/content/dns/zz_gen_virtualname.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: "Virtualname" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: virtualname -dnsprovider: - since: "v4.30.0" - code: "virtualname" - url: "https://www.virtualname.es/" ---- - - - - - - -Configuration for [Virtualname](https://www.virtualname.es/). - - - - -- Code: `virtualname` -- Since: v4.30.0 - - -Here is an example bash command using the Virtualname provider: - -```bash -VIRTUALNAME_TOKEN=xxxxxx \ -lego --dns virtualname -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `VIRTUALNAME_TOKEN` | API token | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `VIRTUALNAME_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `VIRTUALNAME_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | -| `VIRTUALNAME_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) | -| `VIRTUALNAME_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://developers.virtualname.net/#dns) - - - - diff --git a/docs/content/dns/zz_gen_vkcloud.md b/docs/content/dns/zz_gen_vkcloud.md index 76fd557a5..145d44d10 100644 --- a/docs/content/dns/zz_gen_vkcloud.md +++ b/docs/content/dns/zz_gen_vkcloud.md @@ -29,7 +29,7 @@ Here is an example bash command using the VK Cloud provider: VK_CLOUD_PROJECT_ID="" \ VK_CLOUD_USERNAME="" \ VK_CLOUD_PASSWORD="" \ -lego --dns vkcloud -d '*.example.com' -d example.com run +lego --email you@example.com --dns vkcloud --domains "example.org" --domains "*.example.org" run ``` @@ -44,7 +44,7 @@ lego --dns vkcloud -d '*.example.com' -d example.com run | `VK_CLOUD_USERNAME` | Email of VK Cloud account | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -54,12 +54,12 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | `VK_CLOUD_DNS_ENDPOINT` | URL of DNS API. Defaults to https://mcs.mail.ru/public-dns but can be changed for usage with private clouds | | `VK_CLOUD_DOMAIN_NAME` | Openstack users domain name. Defaults to `users` but can be changed for usage with private clouds | | `VK_CLOUD_IDENTITY_ENDPOINT` | URL of OpenStack Auth API, Defaults to https://infra.mail.ru:35357/v3/ but can be changed for usage with private clouds | -| `VK_CLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `VK_CLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `VK_CLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | +| `VK_CLOUD_POLLING_INTERVAL` | Time between DNS propagation check | +| `VK_CLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `VK_CLOUD_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Credential information diff --git a/docs/content/dns/zz_gen_volcengine.md b/docs/content/dns/zz_gen_volcengine.md deleted file mode 100644 index 587ce1e74..000000000 --- a/docs/content/dns/zz_gen_volcengine.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -title: "Volcano Engine/火山引擎" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: volcengine -dnsprovider: - since: "v4.19.0" - code: "volcengine" - url: "https://www.volcengine.com/" ---- - - - - - - -Configuration for [Volcano Engine/火山引擎](https://www.volcengine.com/). - - - - -- Code: `volcengine` -- Since: v4.19.0 - - -Here is an example bash command using the Volcano Engine/火山引擎 provider: - -```bash -VOLC_ACCESSKEY=xxx \ -VOLC_SECRETKEY=yyy \ -lego --dns volcengine -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `VOLC_ACCESSKEY` | Access Key ID (AK) | -| `VOLC_SECRETKEY` | Secret Access Key (SK) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `VOLC_HOST` | API host | -| `VOLC_HTTP_TIMEOUT` | API request timeout in seconds (Default: 15) | -| `VOLC_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | -| `VOLC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 240) | -| `VOLC_REGION` | Region | -| `VOLC_SCHEME` | API scheme | -| `VOLC_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://www.volcengine.com/docs/6758/155086) -- [Go client](https://github.com/volcengine/volc-sdk-golang) - - - - diff --git a/docs/content/dns/zz_gen_vscale.md b/docs/content/dns/zz_gen_vscale.md index c33e2f7b5..37c48ae44 100644 --- a/docs/content/dns/zz_gen_vscale.md +++ b/docs/content/dns/zz_gen_vscale.md @@ -27,7 +27,7 @@ Here is an example bash command using the Vscale provider: ```bash VSCALE_API_TOKEN=xxxxx \ -lego --dns vscale -d '*.example.com' -d example.com run +lego --email you@example.com --dns vscale --domains my.example.org run ``` @@ -40,7 +40,7 @@ lego --dns vscale -d '*.example.com' -d example.com run | `VSCALE_API_TOKEN` | API token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -48,13 +48,13 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | Environment Variable Name | Description | |--------------------------------|-------------| | `VSCALE_BASE_URL` | API endpoint URL | -| `VSCALE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `VSCALE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `VSCALE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `VSCALE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | +| `VSCALE_HTTP_TIMEOUT` | API request timeout | +| `VSCALE_POLLING_INTERVAL` | Time between DNS propagation check | +| `VSCALE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `VSCALE_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_vultr.md b/docs/content/dns/zz_gen_vultr.md index 4160fbcf3..9f2d55c5e 100644 --- a/docs/content/dns/zz_gen_vultr.md +++ b/docs/content/dns/zz_gen_vultr.md @@ -27,7 +27,7 @@ Here is an example bash command using the Vultr provider: ```bash VULTR_API_KEY=xxxxx \ -lego --dns vultr -d '*.example.com' -d example.com run +lego --email you@example.com --dns vultr --domains my.example.org run ``` @@ -40,20 +40,20 @@ lego --dns vultr -d '*.example.com' -d example.com run | `VULTR_API_KEY` | API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `VULTR_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `VULTR_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `VULTR_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `VULTR_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `VULTR_HTTP_TIMEOUT` | API request timeout | +| `VULTR_POLLING_INTERVAL` | Time between DNS propagation check | +| `VULTR_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `VULTR_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_webnames.md b/docs/content/dns/zz_gen_webnames.md index cad02c287..e77e634ba 100644 --- a/docs/content/dns/zz_gen_webnames.md +++ b/docs/content/dns/zz_gen_webnames.md @@ -1,5 +1,5 @@ --- -title: "webnames.ru" +title: "Webnames" date: 2019-03-03T16:39:46+01:00 draft: false slug: webnames @@ -14,7 +14,7 @@ dnsprovider: -Configuration for [webnames.ru](https://www.webnames.ru/). +Configuration for [Webnames](https://www.webnames.ru/). @@ -23,11 +23,11 @@ Configuration for [webnames.ru](https://www.webnames.ru/). - Since: v4.15.0 -Here is an example bash command using the webnames.ru provider: +Here is an example bash command using the Webnames provider: ```bash -WEBNAMESRU_API_KEY=xxxxxx \ -lego --dns webnamesru -d '*.example.com' -d example.com run +WEBNAMES_API_KEY=xxxxxx \ +lego --email you@example.com --dns webnames --domains my.example.org run ``` @@ -37,22 +37,23 @@ lego --dns webnamesru -d '*.example.com' -d example.com run | Environment Variable Name | Description | |-----------------------|-------------| -| `WEBNAMESRU_API_KEY` | Domain API key | +| `WEBNAMES_API_KEY` | Domain API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `WEBNAMESRU_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `WEBNAMESRU_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `WEBNAMESRU_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `WEBNAMES_HTTP_TIMEOUT` | API request timeout | +| `WEBNAMES_POLLING_INTERVAL` | Time between DNS propagation check | +| `WEBNAMES_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `WEBNAMES_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## API Key diff --git a/docs/content/dns/zz_gen_webnamesca.md b/docs/content/dns/zz_gen_webnamesca.md deleted file mode 100644 index 4a7d3794f..000000000 --- a/docs/content/dns/zz_gen_webnamesca.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: "webnames.ca" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: webnamesca -dnsprovider: - since: "v4.28.0" - code: "webnamesca" - url: "https://www.webnames.ca/" ---- - - - - - - -Configuration for [webnames.ca](https://www.webnames.ca/). - - - - -- Code: `webnamesca` -- Since: v4.28.0 - - -Here is an example bash command using the webnames.ca provider: - -```bash -WEBNAMESCA_API_USER="xxx" \ -WEBNAMESCA_API_KEY="yyy" \ -lego --dns webnamesca -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `WEBNAMESCA_API_KEY` | API key | -| `WEBNAMESCA_API_USER` | API username | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `WEBNAMESCA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `WEBNAMESCA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `WEBNAMESCA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `WEBNAMESCA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://www.webnames.ca/_/swagger/index.html) - - - - diff --git a/docs/content/dns/zz_gen_websupport.md b/docs/content/dns/zz_gen_websupport.md index 67ae394d7..cf850e58e 100644 --- a/docs/content/dns/zz_gen_websupport.md +++ b/docs/content/dns/zz_gen_websupport.md @@ -28,7 +28,7 @@ Here is an example bash command using the Websupport provider: ```bash WEBSUPPORT_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ WEBSUPPORT_SECRET="yyyyyyyyyyyyyyyyyyyyy" \ -lego --dns websupport -d '*.example.com' -d example.com run +lego --email myemail@example.com --dns websupport --domains my.example.org run ``` @@ -42,28 +42,28 @@ lego --dns websupport -d '*.example.com' -d example.com run | `WEBSUPPORT_SECRET` | API secret | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `WEBSUPPORT_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `WEBSUPPORT_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `WEBSUPPORT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `WEBSUPPORT_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) | -| `WEBSUPPORT_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) | +| `WEBSUPPORT_HTTP_TIMEOUT` | API request timeout | +| `WEBSUPPORT_POLLING_INTERVAL` | Time between DNS propagation check | +| `WEBSUPPORT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `WEBSUPPORT_SEQUENCE_INTERVAL` | Time between sequential requests | +| `WEBSUPPORT_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information -- [API documentation](https://rest.websupport.sk/v2/docs) +- [API documentation](https://rest.websupport.sk/docs/v1.zone) diff --git a/docs/content/dns/zz_gen_wedos.md b/docs/content/dns/zz_gen_wedos.md index 16139f4d4..bc45dfe1e 100644 --- a/docs/content/dns/zz_gen_wedos.md +++ b/docs/content/dns/zz_gen_wedos.md @@ -28,7 +28,7 @@ Here is an example bash command using the WEDOS provider: ```bash WEDOS_USERNAME=xxxxxxxx \ WEDOS_WAPI_PASSWORD=xxxxxxxx \ -lego --dns wedos -d '*.example.com' -d example.com run +lego --email you@example.com --dns wedos --domains my.example.org run ``` @@ -42,20 +42,20 @@ lego --dns wedos -d '*.example.com' -d example.com run | `WEDOS_WAPI_PASSWORD` | Password needs to be generated and IP allowed in the admin interface | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `WEDOS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `WEDOS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | -| `WEDOS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 600) | -| `WEDOS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | +| `WEDOS_HTTP_TIMEOUT` | API request timeout | +| `WEDOS_POLLING_INTERVAL` | Time between DNS propagation check | +| `WEDOS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `WEDOS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_westcn.md b/docs/content/dns/zz_gen_westcn.md deleted file mode 100644 index a5523b955..000000000 --- a/docs/content/dns/zz_gen_westcn.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: "West.cn/西部数码" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: westcn -dnsprovider: - since: "v4.21.0" - code: "westcn" - url: "https://www.west.cn" ---- - - - - - - -Configuration for [West.cn/西部数码](https://www.west.cn). - - - - -- Code: `westcn` -- Since: v4.21.0 - - -Here is an example bash command using the West.cn/西部数码 provider: - -```bash -WESTCN_USERNAME="xxx" \ -WESTCN_PASSWORD="yyy" \ -lego --dns westcn -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `WESTCN_PASSWORD` | API password | -| `WESTCN_USERNAME` | Username | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `WESTCN_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `WESTCN_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) | -| `WESTCN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) | -| `WESTCN_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://www.west.cn/CustomerCenter/doc/domain_v2.html) - - - - diff --git a/docs/content/dns/zz_gen_yandex.md b/docs/content/dns/zz_gen_yandex.md index 4a1cf1f99..0be857769 100644 --- a/docs/content/dns/zz_gen_yandex.md +++ b/docs/content/dns/zz_gen_yandex.md @@ -27,7 +27,7 @@ Here is an example bash command using the Yandex PDD provider: ```bash YANDEX_PDD_TOKEN= \ -lego --dns yandex -d '*.example.com' -d example.com run +lego --email you@example.com --dns yandex --domains my.example.org run ``` @@ -40,20 +40,20 @@ lego --dns yandex -d '*.example.com' -d example.com run | `YANDEX_PDD_TOKEN` | Basic authentication username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `YANDEX_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `YANDEX_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `YANDEX_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `YANDEX_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 21600) | +| `YANDEX_HTTP_TIMEOUT` | API request timeout | +| `YANDEX_POLLING_INTERVAL` | Time between DNS propagation check | +| `YANDEX_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `YANDEX_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_yandex360.md b/docs/content/dns/zz_gen_yandex360.md index d831fdfc2..79221ac36 100644 --- a/docs/content/dns/zz_gen_yandex360.md +++ b/docs/content/dns/zz_gen_yandex360.md @@ -28,7 +28,7 @@ Here is an example bash command using the Yandex 360 provider: ```bash YANDEX360_OAUTH_TOKEN= \ YANDEX360_ORG_ID= \ -lego --dns yandex360 -d '*.example.com' -d example.com run +lego --email you@example.com --dns yandex360 --domains my.example.org run ``` @@ -42,20 +42,20 @@ lego --dns yandex360 -d '*.example.com' -d example.com run | `YANDEX360_ORG_ID` | The organization ID | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `YANDEX360_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `YANDEX360_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `YANDEX360_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `YANDEX360_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 21600) | +| `YANDEX360_HTTP_TIMEOUT` | API request timeout | +| `YANDEX360_POLLING_INTERVAL` | Time between DNS propagation check | +| `YANDEX360_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `YANDEX360_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_yandexcloud.md b/docs/content/dns/zz_gen_yandexcloud.md index 0564e93d2..61867a92f 100644 --- a/docs/content/dns/zz_gen_yandexcloud.md +++ b/docs/content/dns/zz_gen_yandexcloud.md @@ -28,7 +28,7 @@ Here is an example bash command using the Yandex Cloud provider: ```bash YANDEX_CLOUD_IAM_TOKEN= \ YANDEX_CLOUD_FOLDER_ID= \ -lego --dns yandexcloud -d '*.example.com' -d example.com run +lego --email you@example.com --dns yandexcloud --domains "example.org" --domains "*.example.org" run # --- @@ -41,7 +41,7 @@ YANDEX_CLOUD_IAM_TOKEN=$(echo '{ \ "private_key": "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----" \ }' | base64) \ YANDEX_CLOUD_FOLDER_ID= \ -lego --dns yandexcloud -d '*.example.com' -d example.com run +lego --email you@example.com --dns yandexcloud --domains "example.org" --domains "*.example.org" run ``` @@ -55,19 +55,19 @@ lego --dns yandexcloud -d '*.example.com' -d example.com run | `YANDEX_CLOUD_IAM_TOKEN` | The base64 encoded json which contains information about iam token of service account with `dns.admin` permissions | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `YANDEX_CLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `YANDEX_CLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `YANDEX_CLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) | +| `YANDEX_CLOUD_POLLING_INTERVAL` | Time between DNS propagation check | +| `YANDEX_CLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `YANDEX_CLOUD_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## IAM Token diff --git a/docs/content/dns/zz_gen_zoneedit.md b/docs/content/dns/zz_gen_zoneedit.md deleted file mode 100644 index c7f88b3fe..000000000 --- a/docs/content/dns/zz_gen_zoneedit.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: "ZoneEdit" -date: 2019-03-03T16:39:46+01:00 -draft: false -slug: zoneedit -dnsprovider: - since: "v4.25.0" - code: "zoneedit" - url: "https://www.zoneedit.com" ---- - - - - - - -Configuration for [ZoneEdit](https://www.zoneedit.com). - - - - -- Code: `zoneedit` -- Since: v4.25.0 - - -Here is an example bash command using the ZoneEdit provider: - -```bash -ZONEEDIT_USER="xxxxxxxxxxxxxxxxxxxxx" \ -ZONEEDIT_AUTH_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns zoneedit -d '*.example.com' -d example.com run -``` - - - - -## Credentials - -| Environment Variable Name | Description | -|-----------------------|-------------| -| `ZONEEDIT_AUTH_TOKEN` | Authentication token | -| `ZONEEDIT_USER` | User ID | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - -## Additional Configuration - -| Environment Variable Name | Description | -|--------------------------------|-------------| -| `ZONEEDIT_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `ZONEEDIT_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `ZONEEDIT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | - -The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). - - - - -## More information - -- [API documentation](https://support.zoneedit.com/en/knowledgebase/article/changes-to-dynamic-dns) - - - - diff --git a/docs/content/dns/zz_gen_zoneee.md b/docs/content/dns/zz_gen_zoneee.md index 65678a3dc..473d24281 100644 --- a/docs/content/dns/zz_gen_zoneee.md +++ b/docs/content/dns/zz_gen_zoneee.md @@ -28,7 +28,7 @@ Here is an example bash command using the Zone.ee provider: ```bash ZONEEE_API_USER=xxxxx \ ZONEEE_API_KEY=yyyyy \ -lego --dns zoneee -d '*.example.com' -d example.com run +lego --email you@example.com --dns zoneee --domains my.example.org run ``` @@ -42,7 +42,7 @@ lego --dns zoneee -d '*.example.com' -d example.com run | `ZONEEE_API_USER` | API user | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration @@ -50,12 +50,13 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | Environment Variable Name | Description | |--------------------------------|-------------| | `ZONEEE_ENDPOINT` | API endpoint URL | -| `ZONEEE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `ZONEEE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 5) | -| `ZONEEE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) | +| `ZONEEE_HTTP_TIMEOUT` | API request timeout | +| `ZONEEE_POLLING_INTERVAL` | Time between DNS propagation check | +| `ZONEEE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `ZONEEE_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/dns/zz_gen_zonomi.md b/docs/content/dns/zz_gen_zonomi.md index fd8757f82..99168734b 100644 --- a/docs/content/dns/zz_gen_zonomi.md +++ b/docs/content/dns/zz_gen_zonomi.md @@ -27,7 +27,7 @@ Here is an example bash command using the Zonomi provider: ```bash ZONOMI_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --dns zonomi -d '*.example.com' -d example.com run +lego --email you@example.com --dns zonomi --domains my.example.org run ``` @@ -40,20 +40,20 @@ lego --dns zonomi -d '*.example.com' -d example.com run | `ZONOMI_API_KEY` | User API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| -| `ZONOMI_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) | -| `ZONOMI_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | -| `ZONOMI_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | -| `ZONOMI_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) | +| `ZONOMI_HTTP_TIMEOUT` | API request timeout | +| `ZONOMI_POLLING_INTERVAL` | Time between DNS propagation check | +| `ZONOMI_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `ZONOMI_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{% ref "dns#configuration-and-credentials" %}}). +More information [here]({{< ref "dns#configuration-and-credentials" >}}). diff --git a/docs/content/installation/_index.md b/docs/content/installation/_index.md index a7f2cbdf0..a3e88d3ba 100644 --- a/docs/content/installation/_index.md +++ b/docs/content/installation/_index.md @@ -35,12 +35,11 @@ docker run goacme/lego -h ```bash sudo snap install lego ``` - Note: The snap can only write to the `/var/snap/lego/common/.lego` directory. - [FreeBSD (Ports)](https://www.freshports.org/security/lego) (unofficial): ```bash - pkg install lego + cd /usr/ports/security/lego && make install clean ``` - [Gentoo](https://gitweb.gentoo.org/repo/proj/guru.git/tree/app-crypt/lego) (unofficial): @@ -63,13 +62,6 @@ docker run goacme/lego -h pkg install lego ``` -- [OpenBSD (Ports)](https://openports.pl/path/security/lego) (unofficial): - - ```bash - pkg_add lego - ``` - - ## From sources Requirements: diff --git a/docs/content/usage/cli/General-Instructions.md b/docs/content/usage/cli/General-Instructions.md index 425b79928..6962a79cb 100644 --- a/docs/content/usage/cli/General-Instructions.md +++ b/docs/content/usage/cli/General-Instructions.md @@ -6,11 +6,11 @@ summary: Read this first to clarify some assumptions made by the following guide weight: 1 --- -These examples assume you have [lego installed]({{% ref "installation" %}}). +These examples assume you have [lego installed]({{< ref "installation" >}}). You can get a pre-built binary from the [releases](https://github.com/go-acme/lego/releases) page. The web server examples require that the `lego` binary has permission to bind to ports 80 and 443. -If your environment does not allow you to bind to these ports, please read [Running without root privileges]({{% ref "usage/cli/Options#running-without-root-privileges" %}}) and [Port Usage]({{% ref "usage/cli/Options#port-usage" %}}). +If your environment does not allow you to bind to these ports, please read [Running without root privileges]({{< ref "usage/cli/Options#running-without-root-privileges" >}}) and [Port Usage]({{< ref "usage/cli/Options#port-usage" >}}). Unless otherwise instructed with the `--path` command line flag, lego will look for a directory named `.lego` in the *current working directory*. If you run `cd /dir/a && lego ... run`, lego will create a directory `/dir/a/.lego` where it will save account registration and certificate files into. diff --git a/docs/content/usage/cli/Obtain-a-Certificate.md b/docs/content/usage/cli/Obtain-a-Certificate.md index c7f25dfc0..0d5472e46 100644 --- a/docs/content/usage/cli/Obtain-a-Certificate.md +++ b/docs/content/usage/cli/Obtain-a-Certificate.md @@ -45,34 +45,20 @@ If you're looking for a `cert.pem` and `privkey.pem`, you can just use `example. ## Using a DNS provider If you can't or don't want to start a web server, you need to use a DNS provider. -lego comes with [support for many]({{% ref "dns#dns-providers" %}}) providers, +lego comes with [support for many]({{< ref "dns#dns-providers" >}}) providers, and you need to pick the one where your domain's DNS settings are set up. Typically, this is the registrar where you bought the domain, but in some cases this can be another third-party provider. -For this example, let's assume you have set up Gandi for your domain. +For this example, let's assume you have set up CloudFlare for your domain. Execute this command: ```bash -GANDI_API_KEY=xxx \ -lego --email "you@example.com" --dns gandi --domains "example.org" --domains "*.example.org" run +CLOUDFLARE_EMAIL="you@example.com" \ +CLOUDFLARE_API_KEY="yourprivatecloudflareapikey" \ +lego --email "you@example.com" --dns cloudflare --domains "example.org" run ``` -{{% notice title="For a zone that has multiple SOAs" icon="info-circle" %}} - -This can often be found where your DNS provider has a zone entry for an internal network (i.e. a corporate network, or home LAN) as well as the public internet. -In this case, point lego at an external authoritative server for the zone using the additional parameter `--dns.resolvers`. - -```bash -GANDI_API_KEY=xxx \ -lego --email "you@example.com" --dns gandi --dns.resolvers 9.9.9.9:53 --domains "example.org" --domains "*.example.org" run - -``` - -[More information about resolvers.]({{% ref "options#dns-resolvers-and-challenge-verification" %}}) - -{{% /notice %}} - ## Using a custom certificate signing request (CSR) diff --git a/docs/content/usage/cli/Options.md b/docs/content/usage/cli/Options.md index 7b5df027a..2a6ae9a7c 100644 --- a/docs/content/usage/cli/Options.md +++ b/docs/content/usage/cli/Options.md @@ -85,89 +85,3 @@ In these cases, you can instruct Lego to use a different DNS resolver, using the You should prefer one on the public internet, otherwise you might be susceptible to the same problem. [^apex]: The apex domain is the domain you have registered with your domain registrar. For gTLDs (`.com`, `.fyi`) this is the 2nd level domain, but for ccTLDs, this can either be the 2nd level (`.de`) or 3rd level domain (`.co.uk`). - -## Other options - -### LEGO_CA_CERTIFICATES - -The environment variable `LEGO_CA_CERTIFICATES` allows to specify the path to PEM-encoded CA certificates -that can be used to authenticate an ACME server with an HTTPS certificate not issued by a CA in the system-wide trusted root list. - -Multiple file paths can be added by using `:` (unix) or `;` (Windows) as a separator. - -Example: - -```bash -# On Unix system -LEGO_CA_CERTIFICATES=/foo/cert1.pem:/foo/cert2.pem -``` - -### LEGO_CA_SYSTEM_CERT_POOL - -The environment variable `LEGO_CA_SYSTEM_CERT_POOL` can be used to define if the certificates pool must use a copy of the system cert pool. - -Example: - -```bash -LEGO_CA_SYSTEM_CERT_POOL=true -``` - -### LEGO_CA_SERVER_NAME - -The environment variable `LEGO_CA_SERVER_NAME` allows to specify the CA server name used to authenticate an ACME server -with an HTTPS certificate not issued by a CA in the system-wide trusted root list. - -Example: - -```bash -LEGO_CA_SERVER_NAME=foo -``` - -### LEGO_DISABLE_CNAME_SUPPORT - -By default, lego follows CNAME, the environment variable `LEGO_DISABLE_CNAME_SUPPORT` allows to disable this support. - -Example: - -```bash -LEGO_DISABLE_CNAME_SUPPORT=false -``` - -### LEGO_DEBUG_CLIENT_VERBOSE_ERROR - -The environment variable `LEGO_DEBUG_CLIENT_VERBOSE_ERROR` allows to enrich error messages from some of the DNS clients. - -Example: - -```bash -LEGO_DEBUG_CLIENT_VERBOSE_ERROR=true -``` - -### LEGO_DEBUG_DNS_API_HTTP_CLIENT - -> **⚠️ WARNING: This will expose credentials in the log output! ⚠️** -> -> Do not run this in production environments, or if you can't be sure that logs aren't accessed by third parties or tools (like log collectors). -> -> You have been warned. Here be dragons. - -The environment variable `LEGO_DEBUG_DNS_API_HTTP_CLIENT` allows debugging the DNS API interaction. -It will dump the full request and response to the log output. - -Some DNS providers don't support this option. - -Example: - -```bash -LEGO_DEBUG_DNS_API_HTTP_CLIENT=true -``` - -### LEGO_DEBUG_ACME_HTTP_CLIENT - -The environment variable `LEGO_DEBUG_ACME_HTTP_CLIENT` allows debug the calls to the ACME server. - -Example: - -```bash -LEGO_DEBUG_ACME_HTTP_CLIENT=true -``` diff --git a/docs/content/usage/cli/Renew-a-Certificate.md b/docs/content/usage/cli/Renew-a-Certificate.md index 852c624e4..f630c5eea 100644 --- a/docs/content/usage/cli/Renew-a-Certificate.md +++ b/docs/content/usage/cli/Renew-a-Certificate.md @@ -12,7 +12,7 @@ This guide describes how to renew existing certificates. Certificates issues by Let's Encrypt are valid for a period of 90 days. To avoid certificate errors, you need to ensure that you renew your certificate *before* it expires. -In order to renew a certificate, follow the general instructions laid out under [Obtain a Certificate]({{% ref "usage/cli/Obtain-a-Certificate" %}}), and replace `lego ... run` with `lego ... renew`. +In order to renew a certificate, follow the general instructions laid out under [Obtain a Certificate]({{< ref "usage/cli/Obtain-a-Certificate" >}}), and replace `lego ... run` with `lego ... renew`. Note that the `renew` sub-command supports a slightly different set of some command line flags. ## Using the built-in web server @@ -32,7 +32,7 @@ lego --email="you@example.com" --domains="example.com" --http renew --days 45 ## Using a DNS provider If you can't or don't want to start a web server, you need to use a DNS provider. -lego comes with [support for many]({{% ref "dns#dns-providers" %}}) providers, +lego comes with [support for many]({{< ref "dns#dns-providers" >}}) providers, and you need to pick the one where your domain's DNS settings are set up. Typically, this is the registrar where you bought the domain, but in some cases this can be another third-party provider. @@ -64,7 +64,7 @@ Some information is provided through environment variables: - `LEGO_CERT_PEM_PATH`: (only with `--pem`) the path to the PEM certificate. - `LEGO_CERT_PFX_PATH`: (only with `--pfx`) the path to the PFX certificate. -See [Obtain a Certificate → Use case]({{% ref "usage/cli/Obtain-a-Certificate#use-case" %}}) for an example script. +See [Obtain a Certificate → Use case]({{< ref "usage/cli/Obtain-a-Certificate#use-case" >}}) for an example script. ## Automatic renewal diff --git a/docs/content/usage/cli/examples.md b/docs/content/usage/cli/examples.md index 1a9a40b59..b752b1df5 100644 --- a/docs/content/usage/cli/examples.md +++ b/docs/content/usage/cli/examples.md @@ -11,19 +11,19 @@ hidden: true You'll find the content now at one of these pages: -- Guide: [**How to obtain a certificate**]({{% ref "usage/cli/Obtain-a-Certificate" %}}) +- Guide: [**How to obtain a certificate**]({{< ref "usage/cli/Obtain-a-Certificate" >}}) - Using the built-in web server - Using a DNS provider - Using a custom certificate signing request (CSR) - Using an existing, running web server - Running a script afterward - Use case -- Guide: [**How to renew a certificate**]({{% ref "usage/cli/Renew-a-Certificate" %}}) +- Guide: [**How to renew a certificate**]({{< ref "usage/cli/Renew-a-Certificate" >}}) - Using the built-in web server - Using a DNS provider - Running a script afterward - Automatic renewal -- Reference: [**Command line options**]({{% ref "usage/cli/Options" %}}) +- Reference: [**Command line options**]({{< ref "usage/cli/Options" >}}) - Usage - Let's Encrypt ACME server - Running without root privileges diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index 139143b17..bb57a56bb 100644 --- a/docs/data/zz_cli_help.toml +++ b/docs/data/zz_cli_help.toml @@ -8,7 +8,7 @@ NAME: lego - Let's Encrypt client written in Go USAGE: - lego [global options] command [command options] + lego [global options] command [command options] COMMANDS: run Register an account, then create and install a certificate @@ -22,8 +22,7 @@ GLOBAL OPTIONS: --domains value, -d value [ --domains value, -d value ] Add a domain to the process. Can be specified multiple times. --server value, -s value CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client. (default: "https://acme-v02.api.letsencrypt.org/directory") [$LEGO_SERVER] --accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service. (default: false) - --email value, -m value Email used for registration and recovery contact. [$LEGO_EMAIL] - --disable-cn Disable the use of the common name in the CSR. (default: false) + --email value, -m value Email used for registration and recovery contact. --csr value, -c value Certificate signing request filename, if an external CSR is to be used. --eab Use External Account Binding for account registration. Requires --kid and --hmac. (default: false) [$LEGO_EAB] --kid value Key identifier from External CA. Used for External Account Binding. [$LEGO_EAB_KID] @@ -33,29 +32,22 @@ GLOBAL OPTIONS: --path value Directory to use for storing the data. (default: "./.lego") [$LEGO_PATH] --http Use the HTTP-01 challenge to solve challenges. Can be mixed with other types of challenges. (default: false) --http.port value Set the port and interface to use for HTTP-01 based challenges to listen on. Supported: interface:port or :port. (default: ":80") - --http.delay value Delay between the starts of the HTTP server (use for HTTP-01 based challenges) and the validation of the challenge. (default: 0s) --http.proxy-header value Validate against this HTTP header when solving HTTP-01 based challenges behind a reverse proxy. (default: "Host") --http.webroot value Set the webroot folder to use for HTTP-01 based challenges to write directly to the .well-known/acme-challenge file. This disables the built-in server and expects the given directory to be publicly served with access to .well-known/acme-challenge --http.memcached-host value [ --http.memcached-host value ] Set the memcached host(s) to use for HTTP-01 based challenges. Challenges will be written to all specified hosts. --http.s3-bucket value Set the S3 bucket name to use for HTTP-01 based challenges. Challenges will be written to the S3 bucket. --tls Use the TLS-ALPN-01 challenge to solve challenges. Can be mixed with other types of challenges. (default: false) --tls.port value Set the port and interface to use for TLS-ALPN-01 based challenges to listen on. Supported: interface:port or :port. (default: ":443") - --tls.delay value Delay between the start of the TLS listener (use for TLSALPN-01 based challenges) and the validation of the challenge. (default: 0s) --dns value Solve a DNS-01 challenge using the specified provider. Can be mixed with other types of challenges. Run 'lego dnshelp' for help on usage. - --dns.disable-cp (deprecated) use dns.propagation-disable-ans instead. (default: false) - --dns.propagation-disable-ans By setting this flag to true, disables the need to await propagation of the TXT record to all authoritative name servers. (default: false) - --dns.propagation-rns By setting this flag to true, use all the recursive nameservers to check the propagation of the TXT record. (default: false) - --dns.propagation-wait value By setting this flag, disables all the propagation checks of the TXT record and uses a wait duration instead. (default: 0s) + --dns.disable-cp By setting this flag to true, disables the need to await propagation of the TXT record to all authoritative name servers. (default: false) --dns.resolvers value [ --dns.resolvers value ] Set the resolvers to use for performing (recursive) CNAME resolving and apex domain determination. For DNS-01 challenge verification, the authoritative DNS server is queried directly. Supported: host:port. The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined. --http-timeout value Set the HTTP timeout value to a specific value in seconds. (default: 0) - --tls-skip-verify Skip the TLS verification of the ACME server. (default: false) --dns-timeout value Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name server queries. (default: 10) --pem Generate an additional .pem (base64) file by concatenating the .key and .crt files together. (default: false) --pfx Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together. (default: false) [$LEGO_PFX] --pfx.pass value The password used to encrypt the .pfx (PCKS#12) file. (default: "changeit") [$LEGO_PFX_PASSWORD] --pfx.format value The encoding format to use when encrypting the .pfx (PCKS#12) file. Supported: RC2, DES, SHA256. (default: "RC2") [$LEGO_PFX_FORMAT] --cert.timeout value Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates. (default: 30) - --overall-request-limit value ACME overall requests limit. (default: 18) --user-agent value Add to the user-agent sent to the CA to identify an application embedding lego-cli --help, -h show help """ @@ -67,19 +59,16 @@ NAME: lego run - Register an account, then create and install a certificate USAGE: - lego run [command options] + lego run [command options] [arguments...] OPTIONS: --no-bundle Do not create a certificate bundle by adding the issuers certificate to the new certificate. (default: false) --must-staple Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego. (default: false) --not-before value Set the notBefore field in the certificate (RFC3339 format) --not-after value Set the notAfter field in the certificate (RFC3339 format) - --private-key value Path to private key (in PEM encoding) for the certificate. By default, the private key is generated. --preferred-chain value 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. - --profile value If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one. --always-deactivate-authorizations value Force the authorizations to be relinquished even if the certificate request was successful. --run-hook value Define a hook. The hook is executed when the certificates are effectively created. - --run-hook-timeout value Define the timeout for the hook execution. (default: 2m0s) --help, -h show help """ @@ -90,12 +79,11 @@ NAME: lego renew - Renew a certificate USAGE: - lego renew [command options] + lego renew [command options] [arguments...] OPTIONS: --days value The number of days left on a certificate to renew it. (default: 30) - --dynamic Compute dynamically, based on the lifetime of the certificate(s), when to renew: use 1/3rd of the lifetime left, or 1/2 of the lifetime for short-lived certificates). This supersedes --days and will be the default behavior in Lego v5. (default: false) - --ari-disable Do not use the renewalInfo endpoint (RFC9773) to check if a certificate should be renewed. (default: false) + --ari-enable Use the renewalInfo endpoint (draft-ietf-acme-ari) to check if a certificate should be renewed. (default: false) --ari-wait-to-renew-duration value The maximum duration you're willing to sleep for a renewal time returned by the renewalInfo endpoint. (default: 0s) --reuse-key Used to indicate you want to reuse your current private key for the new certificate. (default: false) --no-bundle Do not create a certificate bundle by adding the issuers certificate to the new certificate. (default: false) @@ -103,12 +91,9 @@ OPTIONS: --not-before value Set the notBefore field in the certificate (RFC3339 format) --not-after value Set the notAfter field in the certificate (RFC3339 format) --preferred-chain value 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. - --profile value If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one. --always-deactivate-authorizations value Force the authorizations to be relinquished even if the certificate request was successful. --renew-hook value Define a hook. The hook is executed only when the certificates are effectively renewed. - --renew-hook-timeout value Define the timeout for the hook execution. (default: 2m0s) --no-random-sleep Do not add a random sleep before the renewal. We do not recommend using this flag if you are doing your renewals in an automated way. (default: false) - --force-cert-domains Check and ensure that the cert's domain list matches those passed in the domains argument. (default: false) --help, -h show help """ @@ -119,7 +104,7 @@ NAME: lego revoke - Revoke a certificate USAGE: - lego revoke [command options] + lego revoke [command options] [arguments...] OPTIONS: --keep, -k Keep the certificates after the revocation instead of archiving them. (default: false) @@ -134,7 +119,7 @@ NAME: lego list - Display certificates and accounts information. USAGE: - lego list [command options] + lego list [command options] [arguments...] OPTIONS: --accounts, -a Display accounts. (default: false) @@ -152,7 +137,7 @@ To display the documentation for a specific DNS provider, run: $ lego dnshelp -c code Supported DNS providers: - acme-dns, active24, alidns, aliesa, allinkl, alwaysdata, anexia, artfiles, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bluecatv2, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, com35, conoha, conohav3, constellix, corenetworks, cpanel, czechia, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dyndnsfree, dynu, easydns, edgecenter, edgedns, edgeone, efficientip, epik, eurodns, excedo, exec, exoscale, f5xc, freemyip, gandi, gandiv5, gcloud, gcore, gigahostno, glesys, godaddy, googledomains, gravity, hetzner, hostingde, hostinger, hostingnl, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ionoscloud, ipv64, ispconfig, ispconfigddns, iwantmyname, jdcloud, joker, keyhelp, leaseweb, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, namesurfer, nearlyfreespeech, neodigit, netcup, netlify, nicmanager, nicru, nifcloud, njalla, nodion, ns1, octenium, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, spaceship, stackpath, syse, technitium, tencentcloud, timewebcloud, todaynic, transip, ultradns, uniteddomains, variomedia, vegadns, vercel, versio, vinyldns, virtualname, vkcloud, volcengine, vscale, vultr, webnames, webnamesca, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi + acme-dns, alidns, allinkl, arvancloud, auroradns, autodns, azure, azuredns, bindman, bluecat, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, conoha, constellix, cpanel, derak, desec, designate, digitalocean, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dynu, easydns, edgedns, efficientip, epik, exec, exoscale, freemyip, gandi, gandiv5, gcloud, gcore, glesys, godaddy, googledomains, hetzner, hostingde, hosttech, httpnet, httpreq, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, liara, lightsail, linode, liquidweb, loopia, luadns, mailinabox, manual, metaname, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, netcup, netlify, nicmanager, nifcloud, njalla, nodion, ns1, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rcodezero, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, servercow, shellrent, simply, sonic, stackpath, tencentcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, vscale, vultr, webnames, websupport, wedos, yandex, yandex360, yandexcloud, zoneee, zonomi More information: https://go-acme.github.io/lego/dns """ diff --git a/docs/go.mod b/docs/go.mod index 2240eb1e6..31794da8f 100644 --- a/docs/go.mod +++ b/docs/go.mod @@ -2,4 +2,4 @@ module github.com/go-acme/lego/docs go 1.20 -require github.com/McShelby/hugo-theme-relearn v0.0.0-20250707094454-9803d5122ebb +require github.com/matcornic/hugo-theme-learn v0.0.0-20211028190410-e817f53d690d // indirect diff --git a/docs/go.sum b/docs/go.sum index b62d5c809..95214cc99 100644 --- a/docs/go.sum +++ b/docs/go.sum @@ -1,2 +1,2 @@ -github.com/McShelby/hugo-theme-relearn v0.0.0-20250707094454-9803d5122ebb h1:iTGWOs8uKUaYmd7+wHRyPGXxt+SS5Bhvx2RRboYRXlI= -github.com/McShelby/hugo-theme-relearn v0.0.0-20250707094454-9803d5122ebb/go.mod h1:mKQQdxZNIlLvAj8X3tMq+RzntIJSr9z7XdzuMomt0IM= +github.com/matcornic/hugo-theme-learn v0.0.0-20211028190410-e817f53d690d h1:p7ktiW/GnHccjB9oO5YGY7us5v/oHmkyF0C7EDZFM3s= +github.com/matcornic/hugo-theme-learn v0.0.0-20211028190410-e817f53d690d/go.mod h1:YoToDcvQxmAFhpEuapKUysBDEBckqDEssqTrmeZ2+uY= diff --git a/docs/hugo.toml b/docs/hugo.toml index fe076a306..7caea1f19 100644 --- a/docs/hugo.toml +++ b/docs/hugo.toml @@ -2,24 +2,49 @@ baseURL = "https://go-acme.github.io/lego/" languageCode = "en-us" title = "Lego" +# Code highlighting settings +pygmentsCodefences = true +pygmentsCodeFencesGuesSsyntax = false +pygmentsOptions = "" +pygmentsStyle = "monokai" +# The monokai stylesheet is included in the base template. +pygmentsUseClasses = true + [permalinks] dns = "/dns/:slug/" [params] + # Prefix URL to edit current page. Will display an "Edit this page" button on top right hand corner of every page. + # Useful to give opportunity to people to create merge request for your doc. + # See the config.toml file from this documentation site to have an example. +# editURL = "" + # Author of the site, will be used in meta information + author = "Lego Team" # Description of the site, will be used in meta information # description = "" # Shows a checkmark for visited pages on the menu showVisitedLinks = true + # Disable search function. It will hide search bar +# disableSearch = false + # Javascript and CSS cache are automatically busted when new version of site is generated. + # Set this to true to disable this behavior (some proxies don't handle well this optimization) +# disableAssetsBusting = false + # Set this to true to disable copy-to-clipboard button for inline code. +# disableInlineCopyToClipBoard = true + # A title for shortcuts in menu is set by default. Set this to true to disable it. +# disableShortcutsTitle = false + # When using mulitlingual website, disable the switch language button. +# disableLanguageSwitchingButton = false + # Hide breadcrumbs in the header and only show the current page title +# disableBreadcrumb = true + # Hide Next and Previous page buttons normally displayed full height beside content +# disableNextPrev = true + # Order sections in menu by "weight" or "title". Default to "weight" +# ordersectionsby = "weight" # Change default color scheme with a variant one. Can be "red", "blue", "green". themeVariant = "blue" custom_css = ["css/theme-custom.css"] disableLandingPageButton = true - hideAuthorEmail = true - hideAuthorName = true - - # Author of the site, will be used in meta information - [params.author] - name = "Lego Team" [Languages] [Languages.en] @@ -44,8 +69,8 @@ title = "Lego" weight = 12 [outputs] - home = ['html', 'rss', 'print'] + home = [ "HTML", "RSS", "JSON"] [module] [[module.imports]] - path = "github.com/McShelby/hugo-theme-relearn" + path = "github.com/matcornic/hugo-theme-learn" diff --git a/docs/layouts/shortcodes/clihelp.html b/docs/layouts/shortcodes/clihelp.html index d315baba1..b3bd95e82 100644 --- a/docs/layouts/shortcodes/clihelp.html +++ b/docs/layouts/shortcodes/clihelp.html @@ -1,12 +1,24 @@ -{{ $tabs := slice }} - -{{ $commands := index $.Site.Data.zz_cli_help "command" }} -{{ range $idx, $tab := $commands }} - {{ $content := (print "```\n" $tab.content "\n```") }} - {{ $tabs = $tabs | append (dict "title" $tab.title "content" ($content | page.RenderString) "icon" "terminal") }} -{{ end }} - -{{ partial "shortcodes/tabs.html" (dict - "page" page - "content" $tabs -) }} +
+
+ {{ $commands := index $.Site.Data.zz_cli_help "command" }} + {{ range $idx, $tab := $commands }} + + {{ end }} +
+
+ {{ range $idx, $tab := $commands }} +
+
{{ .content }}
+
+ {{ end }} +
+
diff --git a/docs/static/.nojekyll b/docs/static/.nojekyll deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/static/images/favicon.png b/docs/static/images/favicon.png deleted file mode 100644 index 04ad9e1ff..000000000 Binary files a/docs/static/images/favicon.png and /dev/null differ diff --git a/docs/static/images/favicon.svg b/docs/static/images/favicon.svg deleted file mode 100644 index 7d4071979..000000000 --- a/docs/static/images/favicon.svg +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/e2e/challenges_test.go b/e2e/challenges_test.go index be1d23131..cbf364c57 100644 --- a/e2e/challenges_test.go +++ b/e2e/challenges_test.go @@ -5,10 +5,8 @@ import ( "crypto/rand" "crypto/rsa" "crypto/x509" - "encoding/pem" "fmt" "os" - "path/filepath" "testing" "time" @@ -23,18 +21,6 @@ import ( "github.com/stretchr/testify/require" ) -const ( - testDomain1 = "acme.localhost" - testDomain2 = "lego.localhost" - testDomain3 = "acme.lego.localhost" - testDomain4 = "légô.localhost" -) - -const ( - testEmail1 = "lego@example.com" - testEmail2 = "acme@example.com" -) - var load = loader.EnvLoader{ PebbleOptions: &loader.CmdOption{ HealthCheckURL: "https://localhost:14000/dir", @@ -43,7 +29,6 @@ var load = loader.EnvLoader{ }, LegoOptions: []string{ "LEGO_CA_CERTIFICATES=./fixtures/certs/pebble.minica.pem", - "LEGO_DEBUG_ACME_HTTP_CLIENT=1", }, } @@ -52,7 +37,7 @@ func TestMain(m *testing.M) { } func TestHelp(t *testing.T) { - output, err := load.RunLegoCombinedOutput("-h") + output, err := load.RunLego("-h") if err != nil { fmt.Fprintf(os.Stderr, "%s\n", output) t.Fatal(err) @@ -64,14 +49,18 @@ func TestHelp(t *testing.T) { func TestChallengeHTTP_Run(t *testing.T) { loader.CleanLegoFiles() - err := load.RunLego( - "-m", testEmail1, + output, err := load.RunLego( + "-m", "hubert@hubert.com", "--accept-tos", "-s", "https://localhost:14000/dir", - "-d", testDomain1, + "-d", "acme.wtf", "--http", "--http.port", ":5002", "run") + + if len(output) > 0 { + fmt.Fprintf(os.Stdout, "%s\n", output) + } if err != nil { t.Fatal(err) } @@ -80,14 +69,18 @@ func TestChallengeHTTP_Run(t *testing.T) { func TestChallengeTLS_Run_Domains(t *testing.T) { loader.CleanLegoFiles() - err := load.RunLego( - "-m", testEmail1, + output, err := load.RunLego( + "-m", "hubert@hubert.com", "--accept-tos", "-s", "https://localhost:14000/dir", - "-d", testDomain1, + "-d", "acme.wtf", "--tls", "--tls.port", ":5001", "run") + + if len(output) > 0 { + fmt.Fprintf(os.Stdout, "%s\n", output) + } if err != nil { t.Fatal(err) } @@ -96,14 +89,18 @@ func TestChallengeTLS_Run_Domains(t *testing.T) { func TestChallengeTLS_Run_IP(t *testing.T) { loader.CleanLegoFiles() - err := load.RunLego( - "-m", testEmail1, + output, err := load.RunLego( + "-m", "hubert@hubert.com", "--accept-tos", "-s", "https://localhost:14000/dir", "-d", "127.0.0.1", "--tls", "--tls.port", ":5001", "run") + + if len(output) > 0 { + fmt.Fprintf(os.Stdout, "%s\n", output) + } if err != nil { t.Fatal(err) } @@ -112,16 +109,18 @@ func TestChallengeTLS_Run_IP(t *testing.T) { func TestChallengeTLS_Run_CSR(t *testing.T) { loader.CleanLegoFiles() - csrPath := createTestCSRFile(t, true) - - err := load.RunLego( - "-m", testEmail1, + output, err := load.RunLego( + "-m", "hubert@hubert.com", "--accept-tos", "-s", "https://localhost:14000/dir", - "-csr", csrPath, + "-csr", "./fixtures/csr.raw", "--tls", "--tls.port", ":5001", "run") + + if len(output) > 0 { + fmt.Fprintf(os.Stdout, "%s\n", output) + } if err != nil { t.Fatal(err) } @@ -130,16 +129,18 @@ func TestChallengeTLS_Run_CSR(t *testing.T) { func TestChallengeTLS_Run_CSR_PEM(t *testing.T) { loader.CleanLegoFiles() - csrPath := createTestCSRFile(t, false) - - err := load.RunLego( - "-m", testEmail1, + output, err := load.RunLego( + "-m", "hubert@hubert.com", "--accept-tos", "-s", "https://localhost:14000/dir", - "-csr", csrPath, + "-csr", "./fixtures/csr.cert", "--tls", "--tls.port", ":5001", "run") + + if len(output) > 0 { + fmt.Fprintf(os.Stdout, "%s\n", output) + } if err != nil { t.Fatal(err) } @@ -148,27 +149,35 @@ func TestChallengeTLS_Run_CSR_PEM(t *testing.T) { func TestChallengeTLS_Run_Revoke(t *testing.T) { loader.CleanLegoFiles() - err := load.RunLego( - "-m", testEmail1, + output, err := load.RunLego( + "-m", "hubert@hubert.com", "--accept-tos", "-s", "https://localhost:14000/dir", - "-d", testDomain2, - "-d", testDomain3, + "-d", "lego.wtf", + "-d", "acme.lego.wtf", "--tls", "--tls.port", ":5001", "run") + + if len(output) > 0 { + fmt.Fprintf(os.Stdout, "%s\n", output) + } if err != nil { t.Fatal(err) } - err = load.RunLego( - "-m", testEmail1, + output, err = load.RunLego( + "-m", "hubert@hubert.com", "--accept-tos", "-s", "https://localhost:14000/dir", - "-d", testDomain2, + "-d", "lego.wtf", "--tls", "--tls.port", ":5001", "revoke") + + if len(output) > 0 { + fmt.Fprintf(os.Stdout, "%s\n", output) + } if err != nil { t.Fatal(err) } @@ -177,26 +186,34 @@ func TestChallengeTLS_Run_Revoke(t *testing.T) { func TestChallengeTLS_Run_Revoke_Non_ASCII(t *testing.T) { loader.CleanLegoFiles() - err := load.RunLego( - "-m", testEmail1, + output, err := load.RunLego( + "-m", "hubert@hubert.com", "--accept-tos", "-s", "https://localhost:14000/dir", - "-d", testDomain4, + "-d", "légô.wtf", "--tls", "--tls.port", ":5001", "run") + + if len(output) > 0 { + fmt.Fprintf(os.Stdout, "%s\n", output) + } if err != nil { t.Fatal(err) } - err = load.RunLego( - "-m", testEmail1, + output, err = load.RunLego( + "-m", "hubert@hubert.com", "--accept-tos", "-s", "https://localhost:14000/dir", - "-d", testDomain4, + "-d", "légô.wtf", "--tls", "--tls.port", ":5001", "revoke") + + if len(output) > 0 { + fmt.Fprintf(os.Stdout, "%s\n", output) + } if err != nil { t.Fatal(err) } @@ -205,7 +222,6 @@ func TestChallengeTLS_Run_Revoke_Non_ASCII(t *testing.T) { func TestChallengeHTTP_Client_Obtain(t *testing.T) { err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem") require.NoError(t, err) - defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() privateKey, err := rsa.GenerateKey(rand.Reader, 2048) @@ -223,100 +239,17 @@ func TestChallengeHTTP_Client_Obtain(t *testing.T) { reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) require.NoError(t, err) - user.registration = reg request := certificate.ObtainRequest{ - Domains: []string{testDomain1}, + Domains: []string{"acme.wtf"}, Bundle: true, } resource, err := client.Certificate.Obtain(request) require.NoError(t, err) require.NotNil(t, resource) - assert.Equal(t, testDomain1, resource.Domain) - assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertURL) - assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertStableURL) - assert.NotEmpty(t, resource.Certificate) - assert.NotEmpty(t, resource.IssuerCertificate) - assert.Empty(t, resource.CSR) -} - -func TestChallengeHTTP_Client_Obtain_profile(t *testing.T) { - err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem") - require.NoError(t, err) - - defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() - - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err, "Could not generate test key") - - user := &fakeUser{privateKey: privateKey} - config := lego.NewConfig(user) - config.CADirURL = load.PebbleOptions.HealthCheckURL - - client, err := lego.NewClient(config) - require.NoError(t, err) - - err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", "5002")) - require.NoError(t, err) - - reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) - require.NoError(t, err) - - user.registration = reg - - request := certificate.ObtainRequest{ - Domains: []string{testDomain1}, - Bundle: true, - Profile: "shortlived", - } - resource, err := client.Certificate.Obtain(request) - require.NoError(t, err) - - require.NotNil(t, resource) - assert.Equal(t, testDomain1, resource.Domain) - assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertURL) - assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertStableURL) - assert.NotEmpty(t, resource.Certificate) - assert.NotEmpty(t, resource.IssuerCertificate) - assert.Empty(t, resource.CSR) -} - -func TestChallengeHTTP_Client_Obtain_emails_csr(t *testing.T) { - err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem") - require.NoError(t, err) - - defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() - - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err, "Could not generate test key") - - user := &fakeUser{privateKey: privateKey} - config := lego.NewConfig(user) - config.CADirURL = load.PebbleOptions.HealthCheckURL - - client, err := lego.NewClient(config) - require.NoError(t, err) - - err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", "5002")) - require.NoError(t, err) - - reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) - require.NoError(t, err) - - user.registration = reg - - request := certificate.ObtainRequest{ - Domains: []string{testDomain1}, - Bundle: true, - EmailAddresses: []string{testEmail1}, - } - resource, err := client.Certificate.Obtain(request) - require.NoError(t, err) - - require.NotNil(t, resource) - assert.Equal(t, testDomain1, resource.Domain) + assert.Equal(t, "acme.wtf", resource.Domain) assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertURL) assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertStableURL) assert.NotEmpty(t, resource.Certificate) @@ -327,7 +260,6 @@ func TestChallengeHTTP_Client_Obtain_emails_csr(t *testing.T) { func TestChallengeHTTP_Client_Obtain_notBefore_notAfter(t *testing.T) { err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem") require.NoError(t, err) - defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() privateKey, err := rsa.GenerateKey(rand.Reader, 2048) @@ -345,13 +277,12 @@ func TestChallengeHTTP_Client_Obtain_notBefore_notAfter(t *testing.T) { reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) require.NoError(t, err) - user.registration = reg now := time.Now().UTC() request := certificate.ObtainRequest{ - Domains: []string{testDomain1}, + Domains: []string{"acme.wtf"}, NotBefore: now.Add(1 * time.Hour), NotAfter: now.Add(2 * time.Hour), Bundle: true, @@ -360,7 +291,7 @@ func TestChallengeHTTP_Client_Obtain_notBefore_notAfter(t *testing.T) { require.NoError(t, err) require.NotNil(t, resource) - assert.Equal(t, testDomain1, resource.Domain) + assert.Equal(t, "acme.wtf", resource.Domain) assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertURL) assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertStableURL) assert.NotEmpty(t, resource.Certificate) @@ -376,7 +307,6 @@ func TestChallengeHTTP_Client_Obtain_notBefore_notAfter(t *testing.T) { func TestChallengeHTTP_Client_Registration_QueryRegistration(t *testing.T) { err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem") require.NoError(t, err) - defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() privateKey, err := rsa.GenerateKey(rand.Reader, 2048) @@ -394,7 +324,6 @@ func TestChallengeHTTP_Client_Registration_QueryRegistration(t *testing.T) { reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) require.NoError(t, err) - user.registration = reg resource, err := client.Registration.QueryRegistration() @@ -410,7 +339,6 @@ func TestChallengeHTTP_Client_Registration_QueryRegistration(t *testing.T) { func TestChallengeTLS_Client_Obtain(t *testing.T) { err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem") require.NoError(t, err) - defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() privateKey, err := rsa.GenerateKey(rand.Reader, 2048) @@ -428,7 +356,6 @@ func TestChallengeTLS_Client_Obtain(t *testing.T) { reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) require.NoError(t, err) - user.registration = reg // https://github.com/letsencrypt/pebble/issues/285 @@ -436,7 +363,7 @@ func TestChallengeTLS_Client_Obtain(t *testing.T) { require.NoError(t, err, "Could not generate test key") request := certificate.ObtainRequest{ - Domains: []string{testDomain1}, + Domains: []string{"acme.wtf"}, Bundle: true, PrivateKey: privateKeyCSR, } @@ -444,7 +371,7 @@ func TestChallengeTLS_Client_Obtain(t *testing.T) { require.NoError(t, err) require.NotNil(t, resource) - assert.Equal(t, testDomain1, resource.Domain) + assert.Equal(t, "acme.wtf", resource.Domain) assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertURL) assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertStableURL) assert.NotEmpty(t, resource.Certificate) @@ -455,7 +382,6 @@ func TestChallengeTLS_Client_Obtain(t *testing.T) { func TestChallengeTLS_Client_ObtainForCSR(t *testing.T) { err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem") require.NoError(t, err) - defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() privateKey, err := rsa.GenerateKey(rand.Reader, 2048) @@ -473,10 +399,12 @@ func TestChallengeTLS_Client_ObtainForCSR(t *testing.T) { reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) require.NoError(t, err) - user.registration = reg - csr, err := x509.ParseCertificateRequest(createTestCSR(t)) + csrRaw, err := os.ReadFile("./fixtures/csr.raw") + require.NoError(t, err) + + csr, err := x509.ParseCertificateRequest(csrRaw) require.NoError(t, err) resource, err := client.Certificate.ObtainForCSR(certificate.ObtainForCSRRequest{ @@ -486,50 +414,7 @@ func TestChallengeTLS_Client_ObtainForCSR(t *testing.T) { require.NoError(t, err) require.NotNil(t, resource) - assert.Equal(t, testDomain1, resource.Domain) - assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertURL) - assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertStableURL) - assert.NotEmpty(t, resource.Certificate) - assert.NotEmpty(t, resource.IssuerCertificate) - assert.NotEmpty(t, resource.CSR) -} - -func TestChallengeTLS_Client_ObtainForCSR_profile(t *testing.T) { - err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem") - require.NoError(t, err) - - defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() - - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err, "Could not generate test key") - - user := &fakeUser{privateKey: privateKey} - config := lego.NewConfig(user) - config.CADirURL = load.PebbleOptions.HealthCheckURL - - client, err := lego.NewClient(config) - require.NoError(t, err) - - err = client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer("", "5001")) - require.NoError(t, err) - - reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) - require.NoError(t, err) - - user.registration = reg - - csr, err := x509.ParseCertificateRequest(createTestCSR(t)) - require.NoError(t, err) - - resource, err := client.Certificate.ObtainForCSR(certificate.ObtainForCSRRequest{ - CSR: csr, - Bundle: true, - Profile: "shortlived", - }) - require.NoError(t, err) - - require.NotNil(t, resource) - assert.Equal(t, testDomain1, resource.Domain) + assert.Equal(t, "acme.wtf", resource.Domain) assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertURL) assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertStableURL) assert.NotEmpty(t, resource.Certificate) @@ -540,7 +425,6 @@ func TestChallengeTLS_Client_ObtainForCSR_profile(t *testing.T) { func TestRegistrar_UpdateAccount(t *testing.T) { err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem") require.NoError(t, err) - defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() privateKey, err := rsa.GenerateKey(rand.Reader, 2048) @@ -548,7 +432,7 @@ func TestRegistrar_UpdateAccount(t *testing.T) { user := &fakeUser{ privateKey: privateKey, - email: testEmail1, + email: "foo@example.com", } config := lego.NewConfig(user) config.CADirURL = load.PebbleOptions.HealthCheckURL @@ -559,13 +443,13 @@ func TestRegistrar_UpdateAccount(t *testing.T) { regOptions := registration.RegisterOptions{TermsOfServiceAgreed: true} reg, err := client.Registration.Register(regOptions) require.NoError(t, err) - require.Equal(t, []string{"mailto:" + testEmail1}, reg.Body.Contact) + require.Equal(t, []string{"mailto:foo@example.com"}, reg.Body.Contact) user.registration = reg - user.email = testEmail2 + user.email = "bar@example.com" resource, err := client.Registration.UpdateRegistration(regOptions) require.NoError(t, err) - require.Equal(t, []string{"mailto:" + testEmail2}, resource.Body.Contact) + require.Equal(t, []string{"mailto:bar@example.com"}, resource.Body.Contact) require.Equal(t, reg.URI, resource.URI) } @@ -578,53 +462,3 @@ type fakeUser struct { func (f *fakeUser) GetEmail() string { return f.email } func (f *fakeUser) GetRegistration() *registration.Resource { return f.registration } func (f *fakeUser) GetPrivateKey() crypto.PrivateKey { return f.privateKey } - -func createTestCSRFile(t *testing.T, raw bool) string { - t.Helper() - - csr := createTestCSR(t) - - if raw { - filename := filepath.Join(t.TempDir(), "csr.raw") - - fileRaw, err := os.Create(filename) - require.NoError(t, err) - - defer fileRaw.Close() - - _, err = fileRaw.Write(csr) - require.NoError(t, err) - - return filename - } - - filename := filepath.Join(t.TempDir(), "csr.cert") - - file, err := os.Create(filename) - require.NoError(t, err) - - defer file.Close() - - _, err = file.Write(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr})) - require.NoError(t, err) - - return filename -} - -func createTestCSR(t *testing.T) []byte { - t.Helper() - - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) - require.NoError(t, err) - - csr, err := certcrypto.CreateCSR(privateKey, certcrypto.CSROptions{ - Domain: testDomain1, - SAN: []string{ - testDomain1, - testDomain2, - }, - }) - require.NoError(t, err) - - return csr -} diff --git a/e2e/dnschallenge/dns_challenges_test.go b/e2e/dnschallenge/dns_challenges_test.go index 9dd9ab0d6..aab3bb38c 100644 --- a/e2e/dnschallenge/dns_challenges_test.go +++ b/e2e/dnschallenge/dns_challenges_test.go @@ -18,11 +18,6 @@ import ( "github.com/stretchr/testify/require" ) -const ( - testDomain1 = "légo.localhost" - testDomain2 = "*.légo.localhost" -) - var load = loader.EnvLoader{ PebbleOptions: &loader.CmdOption{ HealthCheckURL: "https://localhost:15000/dir", @@ -33,7 +28,6 @@ var load = loader.EnvLoader{ LegoOptions: []string{ "LEGO_CA_CERTIFICATES=../fixtures/certs/pebble.minica.pem", "EXEC_PATH=../fixtures/update-dns.sh", - "LEGO_DEBUG_ACME_HTTP_CLIENT=1", }, ChallSrv: &loader.CmdOption{ Args: []string{"-http01", ":5012", "-tlsalpn01", ":5011"}, @@ -45,7 +39,7 @@ func TestMain(m *testing.M) { } func TestDNSHelp(t *testing.T) { - output, err := load.RunLegoCombinedOutput("dnshelp") + output, err := load.RunLego("dnshelp") if err != nil { fmt.Fprintf(os.Stderr, "%s\n", output) t.Fatal(err) @@ -57,15 +51,20 @@ func TestDNSHelp(t *testing.T) { func TestChallengeDNS_Run(t *testing.T) { loader.CleanLegoFiles() - err := load.RunLego( + output, err := load.RunLego( + "-m", "hubert@hubert.com", "--accept-tos", "--dns", "exec", "--dns.resolvers", ":8053", "--dns.disable-cp", "-s", "https://localhost:15000/dir", - "-d", testDomain2, - "-d", testDomain1, + "-d", "*.légo.acme", + "-d", "légo.acme", "run") + + if len(output) > 0 { + fmt.Fprintf(os.Stdout, "%s\n", output) + } if err != nil { t.Fatal(err) } @@ -74,12 +73,10 @@ func TestChallengeDNS_Run(t *testing.T) { func TestChallengeDNS_Client_Obtain(t *testing.T) { err := os.Setenv("LEGO_CA_CERTIFICATES", "../fixtures/certs/pebble.minica.pem") require.NoError(t, err) - defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() err = os.Setenv("EXEC_PATH", "../fixtures/update-dns.sh") require.NoError(t, err) - defer func() { _ = os.Unsetenv("EXEC_PATH") }() privateKey, err := rsa.GenerateKey(rand.Reader, 2048) @@ -97,15 +94,14 @@ func TestChallengeDNS_Client_Obtain(t *testing.T) { err = client.Challenge.SetDNS01Provider(provider, dns01.AddRecursiveNameservers([]string{":8053"}), - dns01.DisableAuthoritativeNssPropagationRequirement()) + dns01.DisableCompletePropagationRequirement()) require.NoError(t, err) reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) require.NoError(t, err) - user.registration = reg - domains := []string{testDomain2, testDomain1} + domains := []string{"*.légo.acme", "légo.acme"} // https://github.com/letsencrypt/pebble/issues/285 privateKeyCSR, err := rsa.GenerateKey(rand.Reader, 2048) @@ -120,65 +116,7 @@ func TestChallengeDNS_Client_Obtain(t *testing.T) { require.NoError(t, err) require.NotNil(t, resource) - assert.Equal(t, "*.xn--lgo-bma.localhost", resource.Domain) - assert.Regexp(t, `https://localhost:15000/certZ/[\w\d]{14,}`, resource.CertURL) - assert.Regexp(t, `https://localhost:15000/certZ/[\w\d]{14,}`, resource.CertStableURL) - assert.NotEmpty(t, resource.Certificate) - assert.NotEmpty(t, resource.IssuerCertificate) - assert.Empty(t, resource.CSR) -} - -func TestChallengeDNS_Client_Obtain_profile(t *testing.T) { - err := os.Setenv("LEGO_CA_CERTIFICATES", "../fixtures/certs/pebble.minica.pem") - require.NoError(t, err) - - defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() - - err = os.Setenv("EXEC_PATH", "../fixtures/update-dns.sh") - require.NoError(t, err) - - defer func() { _ = os.Unsetenv("EXEC_PATH") }() - - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err, "Could not generate test key") - - user := &fakeUser{privateKey: privateKey} - config := lego.NewConfig(user) - config.CADirURL = "https://localhost:15000/dir" - - client, err := lego.NewClient(config) - require.NoError(t, err) - - provider, err := dns.NewDNSChallengeProviderByName("exec") - require.NoError(t, err) - - err = client.Challenge.SetDNS01Provider(provider, - dns01.AddRecursiveNameservers([]string{":8053"}), - dns01.DisableAuthoritativeNssPropagationRequirement()) - require.NoError(t, err) - - reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) - require.NoError(t, err) - - user.registration = reg - - domains := []string{testDomain2, testDomain1} - - // https://github.com/letsencrypt/pebble/issues/285 - privateKeyCSR, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err, "Could not generate test key") - - request := certificate.ObtainRequest{ - Domains: domains, - Bundle: true, - PrivateKey: privateKeyCSR, - Profile: "shortlived", - } - resource, err := client.Certificate.Obtain(request) - require.NoError(t, err) - - require.NotNil(t, resource) - assert.Equal(t, "*.xn--lgo-bma.localhost", resource.Domain) + assert.Equal(t, "*.xn--lgo-bma.acme", resource.Domain) assert.Regexp(t, `https://localhost:15000/certZ/[\w\d]{14,}`, resource.CertURL) assert.Regexp(t, `https://localhost:15000/certZ/[\w\d]{14,}`, resource.CertStableURL) assert.NotEmpty(t, resource.Certificate) diff --git a/e2e/fixtures/certs/localhost/cert.pem b/e2e/fixtures/certs/localhost/cert.pem index d81d29e70..2866a2b48 100644 --- a/e2e/fixtures/certs/localhost/cert.pem +++ b/e2e/fixtures/certs/localhost/cert.pem @@ -1,20 +1,19 @@ -----BEGIN CERTIFICATE----- -MIIDMDCCAhigAwIBAgIILDt8c2fMw2IwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE -AxMVbWluaWNhIHJvb3QgY2EgNTM0NWU2MB4XDTI1MDkwMzIzNDAwNVoXDTI3MTAw -MzIzNDAwNVowFDESMBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAmxTFtw113RK70H9pQmdKs9AxhFmnQ6BdDtp3jOZlWlUO -0BltMXOUML5905etgtCbcC6RdKRtgSAiDfgx3VWiFMJH++4gUtnaB9SN8GhNSPBp -FfSa2JhWPo9HQNUsAZqlGTV4SzcGRqtWvdZxUiOfQ2TcvyXIqsaD19ivvqI1NhT6 -bl3tredTZlzLLM6Wvkw6hfyHrJAPQP8LOlCIeDM4YIce6Gstv6qo9iCD4wJiY4u9 -5HVL7RK8t8JpZAb7VR+dPhbHEvVpjwuYd5Q05OZ280gFyrhbrKLbqst104GOQT4k -QMJGWxGONyTX6np0Dx6O5jU7dvYvjVVawbJwGuaL6wIDAQABo3oweDAOBgNVHQ8B -Af8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAfBgNV -HSMEGDAWgBSu8RGpErgYUoYnQuwCq+/ggTiEjDAiBgNVHREEGzAZgglsb2NhbGhv -c3SCBnBlYmJsZYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAAB0gkekXCNOwqWmY -vQ2lLJ8Zk2WzQ9B+VOC27IgxEEuskZyCpyXAbJB9sCGQWZhAARyaI4SPRGGagcug -d1SwDWdPGeSJzF3aDnXDYoP9Zw2KqiqVZTngeoiw8Yn0F8PNriANwRLybouX7mMc -4V7T5+2k4SUs7pFH4KO0a0XBCcjXDjdKuBljftRTXCHzJzfRtmieCCuZlpnp5sHx -hKa/uxKGyyZB+4Y3MrzsiQSCBOr9G4TH9RofmNcawl+tsVe08zLV/XVhrbakKEs7 -Y7MGHSj3BkPFF32NObc0znqWzTaUD9hU+rXWGANM4sXd4dagdnxfrb7i0WYhcUFj -9Try8Q== +MIIDGzCCAgOgAwIBAgIIbEfayDFsBtwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVbWluaWNhIHJvb3QgY2EgMjRlMmRiMCAXDTE3MTIwNjE5NDIxMFoYDzIxMDcx +MjA2MTk0MjEwWjAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCbFMW3DXXdErvQf2lCZ0qz0DGEWadDoF0O2neM5mVa +VQ7QGW0xc5Qwvn3Tl62C0JtwLpF0pG2BICIN+DHdVaIUwkf77iBS2doH1I3waE1I +8GkV9JrYmFY+j0dA1SwBmqUZNXhLNwZGq1a91nFSI59DZNy/JciqxoPX2K++ojU2 +FPpuXe2t51NmXMsszpa+TDqF/IeskA9A/ws6UIh4Mzhghx7oay2/qqj2IIPjAmJj +i73kdUvtEry3wmlkBvtVH50+FscS9WmPC5h3lDTk5nbzSAXKuFusotuqy3XTgY5B +PiRAwkZbEY43JNfqenQPHo7mNTt29i+NVVrBsnAa5ovrAgMBAAGjYzBhMA4GA1Ud +DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0T +AQH/BAIwADAiBgNVHREEGzAZgglsb2NhbGhvc3SCBnBlYmJsZYcEfwAAATANBgkq +hkiG9w0BAQsFAAOCAQEAYIkXff8H28KS0KyLHtbbSOGU4sujHHVwiVXSATACsNAE +D0Qa8hdtTQ6AUqA6/n8/u1tk0O4rPE/cTpsM3IJFX9S3rZMRsguBP7BSr1Lq/XAB +7JP/CNHt+Z9aKCKcg11wIX9/B9F7pyKM3TdKgOpqXGV6TMuLjg5PlYWI/07lVGFW +/mSJDRs8bSCFmbRtEqc4lpwlrpz+kTTnX6G7JDLfLWYw/xXVqwFfdengcDTHCc8K +wtgGq/Gu6vcoBxIO3jaca+OIkMfxxXmGrcNdseuUCa3RMZ8Qy03DqGu6Y6XQyK4B +W8zIG6H9SVKkAznM2yfYhW8v2ktcaZ95/OBHY97ZIw== -----END CERTIFICATE----- diff --git a/e2e/fixtures/certs/pebble.minica.pem b/e2e/fixtures/certs/pebble.minica.pem index 5578b5b55..a69a4c419 100644 --- a/e2e/fixtures/certs/pebble.minica.pem +++ b/e2e/fixtures/certs/pebble.minica.pem @@ -1,20 +1,19 @@ -----BEGIN CERTIFICATE----- -MIIDPzCCAiegAwIBAgIIU0Xm9UFdQxUwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE -AxMVbWluaWNhIHJvb3QgY2EgNTM0NWU2MCAXDTI1MDkwMzIzNDAwNVoYDzIxMjUw -OTAzMjM0MDA1WjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSA1MzQ1ZTYwggEi +MIIDCTCCAfGgAwIBAgIIJOLbes8sTr4wDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVbWluaWNhIHJvb3QgY2EgMjRlMmRiMCAXDTE3MTIwNjE5NDIxMFoYDzIxMTcx +MjA2MTk0MjEwWjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSAyNGUyZGIwggEi MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5WgZNoVJandj43kkLyU50vzCZ alozvdRo3OFiKoDtmqKPNWRNO2hC9AUNxTDJco51Yc42u/WV3fPbbhSznTiOOVtn Ajm6iq4I5nZYltGGZetGDOQWr78y2gWY+SG078MuOO2hyDIiKtVc3xiXYA+8Hluu 9F8KbqSS1h55yxZ9b87eKR+B0zu2ahzBCIHKmKWgc6N13l7aDxxY3D6uq8gtJRU0 toumyLbdzGcupVvjbjDP11nl07RESDWBLG1/g3ktJvqIa4BWgU2HMh4rND6y8OD3 Hy3H8MY6CElL+MOCbFJjWqhtOxeFyZZV9q3kYnk9CAuQJKMEGuN4GU6tzhW1AgMB -AAGjezB5MA4GA1UdDwEB/wQEAwIChDATBgNVHSUEDDAKBggrBgEFBQcDATASBgNV -HRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSu8RGpErgYUoYnQuwCq+/ggTiEjDAf -BgNVHSMEGDAWgBSu8RGpErgYUoYnQuwCq+/ggTiEjDANBgkqhkiG9w0BAQsFAAOC -AQEAXDVYov1+f6EL7S41LhYQkEX/GyNNzsEvqxE9U0+3Iri5JfkcNOiA9O9L6Z+Y -bqcsXV93s3vi4r4WSWuc//wHyJYrVe5+tK4nlFpbJOvfBUtnoBDyKNxXzZCxFJVh -f9uc8UejRfQMFbDbhWY/x83y9BDufJHHq32OjCIN7gp2UR8rnfYvlz7Zg4qkJBsn -DG4dwd+pRTCFWJOVIG0JoNhK3ZmE7oJ1N4H38XkZ31NPcMksKxpsLLIS9+mosZtg -4olL7tMPJklx5ZaeMFaKRDq4Gdxkbw4+O4vRgNm3Z8AXWKknOdfgdpqLUPPhRcP4 -v1lhy71EhBuXXwRQJry0lTdF+w== +AAGjRTBDMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB +BQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsFAAOCAQEAF85v +d40HK1ouDAtWeO1PbnWfGEmC5Xa478s9ddOd9Clvp2McYzNlAFfM7kdcj6xeiNhF +WPIfaGAi/QdURSL/6C1KsVDqlFBlTs9zYfh2g0UXGvJtj1maeih7zxFLvet+fqll +xseM4P9EVJaQxwuK/F78YBt0tCNfivC6JNZMgxKF59h0FBpH70ytUSHXdz7FKwix +Mfn3qEb9BXSk0Q3prNV5sOV3vgjEtB4THfDxSz9z3+DepVnW3vbbqwEbkXdk3j82 +2muVldgOUgTwK8eT+XdofVdntzU/kzygSAtAQwLJfn51fS1GvEcYGBc1bDryIqmF +p9BI7gVKtWSZYegicA== -----END CERTIFICATE----- diff --git a/e2e/fixtures/csr.cert b/e2e/fixtures/csr.cert new file mode 100644 index 000000000..cece7ddec --- /dev/null +++ b/e2e/fixtures/csr.cert @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICfjCCAWYCAQAwEzERMA8GA1UEAxMIYWNtZS53dGYwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDAhXnho1w9OPHWs4YSMahYbG4Ui1K6hsHytBZfhsz0 +09igSWzHMEFZYHZJVuSr60enuJSZRhgwDjfhQWSUgHgKItLPnlNVYM6RhVaW0WfT +w6CpmE2AuH3WuQbrR2he1Nt0xfUJla+VWOFZuW7GhgBiV5iWBvdLv6Ztgh8eATjo +2vG2R+KuSUzrm6h+sb3nUR28OYunZ3vESjNwnL3/D/1th2rFpe3EA3em1HArJdXN +F4eclciun5Js17AS9tdoHEEZMMBWyViiuz3CQlh+YD2qAvqaubanWNa+r+iijMvd +4HlDHC99LTk6TJoSKoL+E/OGKmntLqmBJ1UrCFgvnw3DAgMBAAGgJjAkBgkqhkiG +9w0BCQ4xFzAVMBMGA1UdEQQMMAqCCGFjbWUud3RmMA0GCSqGSIb3DQEBCwUAA4IB +AQAfBLR8njftxf15V49szNsgNaG7Y5UQFwgl8pyiIaanGvX1DE0BtU1RB/w7itzX +wW5W/wjielEbs1XkI2uz3hkebvHVA1QpA7bbrX01WonS18xCkiRDj8ZqFEG4vEGa +HswzGUfq2v0gCOIPpVGE+8Q2Y7In5zwEfev+5DkHox4/vgwMhyPMI+y7jKtdG/dV +U58SFnt/F1raoSmR6vfDcAFXm/L8LXEkxqqefFbhiRHRqQar1Wr15BH//swmNzEW +5SVCCHcyIqreSua8uPjBcJ8aYVLniX6DMRyYv4ij/PSvSQy9xJDewLqR235WfTd/ +tk4hhJaqizKDpsvB+UFod5o5 +-----END CERTIFICATE REQUEST----- diff --git a/e2e/fixtures/csr.raw b/e2e/fixtures/csr.raw new file mode 100644 index 000000000..f4bb701cd Binary files /dev/null and b/e2e/fixtures/csr.raw differ diff --git a/e2e/fixtures/pebble-config-dns.json b/e2e/fixtures/pebble-config-dns.json index dd5b63142..4834825a4 100644 --- a/e2e/fixtures/pebble-config-dns.json +++ b/e2e/fixtures/pebble-config-dns.json @@ -4,16 +4,6 @@ "certificate": "fixtures/certs/localhost/cert.pem", "privateKey": "fixtures/certs/localhost/key.pem", "httpPort": 5004, - "tlsPort": 5003, - "profiles": { - "default": { - "description": "The profile you know and love", - "validityPeriod": 7776000 - }, - "shortlived": { - "description": "A short-lived cert profile, without actual enforcement", - "validityPeriod": 518400 - } - } + "tlsPort": 5003 } } diff --git a/e2e/fixtures/pebble-config.json b/e2e/fixtures/pebble-config.json index dcf659b4c..f2abe6ab8 100644 --- a/e2e/fixtures/pebble-config.json +++ b/e2e/fixtures/pebble-config.json @@ -4,16 +4,6 @@ "certificate": "fixtures/certs/localhost/cert.pem", "privateKey": "fixtures/certs/localhost/key.pem", "httpPort": 5002, - "tlsPort": 5001, - "profiles": { - "default": { - "description": "The profile you know and love", - "validityPeriod": 7776000 - }, - "shortlived": { - "description": "A short-lived cert profile, without actual enforcement", - "validityPeriod": 518400 - } - } + "tlsPort": 5001 } } diff --git a/e2e/loader/loader.go b/e2e/loader/loader.go index 3e63302a3..7e8ff539f 100644 --- a/e2e/loader/loader.go +++ b/e2e/loader/loader.go @@ -1,9 +1,7 @@ package loader import ( - "bufio" "bytes" - "context" "crypto/tls" "errors" "fmt" @@ -17,7 +15,6 @@ import ( "time" "github.com/go-acme/lego/v4/platform/wait" - "github.com/ldez/grignotin/goenv" ) const ( @@ -43,14 +40,12 @@ func (l *EnvLoader) MainTest(m *testing.M) int { if _, e2e := os.LookupEnv("LEGO_E2E_TESTS"); !e2e { fmt.Fprintln(os.Stderr, "skipping test: e2e tests are disabled. (no 'LEGO_E2E_TESTS' env var)") fmt.Println("PASS") - return 0 } if _, err := exec.LookPath("git"); err != nil { fmt.Fprintln(os.Stderr, "skipping because git command not found") fmt.Println("PASS") - return 0 } @@ -58,7 +53,6 @@ func (l *EnvLoader) MainTest(m *testing.M) int { if _, err := exec.LookPath(cmdNamePebble); err != nil { fmt.Fprintln(os.Stderr, "skipping because pebble binary not found") fmt.Println("PASS") - return 0 } } @@ -67,7 +61,6 @@ func (l *EnvLoader) MainTest(m *testing.M) int { if _, err := exec.LookPath(cmdNameChallSrv); err != nil { fmt.Fprintln(os.Stderr, "skipping because challtestsrv binary not found") fmt.Println("PASS") - return 0 } } @@ -80,7 +73,6 @@ func (l *EnvLoader) MainTest(m *testing.M) int { legoBinary, tearDown, err := buildLego() defer tearDown() - if err != nil { fmt.Fprintln(os.Stderr, err) return 1 @@ -95,7 +87,7 @@ func (l *EnvLoader) MainTest(m *testing.M) int { return m.Run() } -func (l *EnvLoader) RunLegoCombinedOutput(arg ...string) ([]byte, error) { +func (l *EnvLoader) RunLego(arg ...string) ([]byte, error) { cmd := exec.Command(l.lego, arg...) cmd.Env = l.LegoOptions @@ -104,44 +96,12 @@ func (l *EnvLoader) RunLegoCombinedOutput(arg ...string) ([]byte, error) { return cmd.CombinedOutput() } -func (l *EnvLoader) RunLego(arg ...string) error { - cmd := exec.Command(l.lego, arg...) - cmd.Env = l.LegoOptions - - fmt.Printf("$ %s\n", strings.Join(cmd.Args, " ")) - - stdout, err := cmd.StdoutPipe() - if err != nil { - return fmt.Errorf("create pipe: %w", err) - } - - cmd.Stderr = cmd.Stdout - - err = cmd.Start() - if err != nil { - return fmt.Errorf("start command: %w", err) - } - - scanner := bufio.NewScanner(stdout) - for scanner.Scan() { - println(scanner.Text()) - } - - err = cmd.Wait() - if err != nil { - return fmt.Errorf("wait command: %w", err) - } - - return nil -} - func (l *EnvLoader) launchPebble() func() { if l.PebbleOptions == nil { return func() {} } pebble, outPebble := l.cmdPebble() - go func() { err := pebble.Run() if err != nil { @@ -154,7 +114,6 @@ func (l *EnvLoader) launchPebble() func() { if err != nil { fmt.Println(err) } - fmt.Println(outPebble.String()) } } @@ -167,13 +126,11 @@ func (l *EnvLoader) cmdPebble() (*exec.Cmd, *bytes.Buffer) { if err != nil { panic(err) } - cmd.Dir = dir fmt.Printf("$ %s\n", strings.Join(cmd.Args, " ")) var b bytes.Buffer - cmd.Stdout = &b cmd.Stderr = &b @@ -182,7 +139,6 @@ func (l *EnvLoader) cmdPebble() (*exec.Cmd, *bytes.Buffer) { func pebbleHealthCheck(options *CmdOption) { client := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}} - err := wait.For("pebble", 10*time.Second, 500*time.Millisecond, func() (bool, error) { resp, err := client.Get(options.HealthCheckURL) if err != nil { @@ -206,7 +162,6 @@ func (l *EnvLoader) launchChallSrv() func() { } challtestsrv, outChalSrv := l.cmdChallSrv() - go func() { err := challtestsrv.Run() if err != nil { @@ -219,7 +174,6 @@ func (l *EnvLoader) launchChallSrv() func() { if err != nil { fmt.Println(err) } - fmt.Println(outChalSrv.String()) } } @@ -230,7 +184,6 @@ func (l *EnvLoader) cmdChallSrv() (*exec.Cmd, *bytes.Buffer) { fmt.Printf("$ %s\n", strings.Join(cmd.Args, " ")) var b bytes.Buffer - cmd.Stdout = &b cmd.Stderr = &b @@ -242,7 +195,6 @@ func buildLego() (string, func(), error) { if err != nil { return "", func() {}, err } - defer func() { _ = os.Chdir(here) }() buildPath, err := os.MkdirTemp("", "lego_test") @@ -276,7 +228,6 @@ func buildLego() (string, func(), error) { return binary, func() { _ = os.RemoveAll(buildPath) - CleanLegoFiles() }, nil } @@ -298,7 +249,6 @@ func build(binary string) error { if err != nil { return err } - cmd := exec.Command(toolPath, "build", "-o", binary) output, err := cmd.CombinedOutput() @@ -329,13 +279,8 @@ func goTool() (string, error) { exeSuffix = ".exe" } - goRoot, err := goenv.GetOne(context.Background(), goenv.GOROOT) - if err != nil { - return "", fmt.Errorf("cannot find go root: %w", err) - } - - path := filepath.Join(goRoot, "bin", "go"+exeSuffix) - if _, err = os.Stat(path); err == nil { + path := filepath.Join(runtime.GOROOT(), "bin", "go"+exeSuffix) + if _, err := os.Stat(path); err == nil { return path, nil } @@ -350,7 +295,6 @@ func goTool() (string, error) { func CleanLegoFiles() { cmd := exec.Command("rm", "-rf", ".lego") fmt.Printf("$ %s\n", strings.Join(cmd.Args, " ")) - output, err := cmd.CombinedOutput() if err != nil { fmt.Println(string(output)) diff --git a/e2e/readme.md b/e2e/readme.md index 171170507..746b9d726 100644 --- a/e2e/readme.md +++ b/e2e/readme.md @@ -1,9 +1,20 @@ # E2E tests +How to run: + +- Add the following entries to your `/etc/hosts`: +``` +127.0.0.1 acme.wtf +127.0.0.1 lego.wtf +127.0.0.1 acme.lego.wtf +127.0.0.1 légô.wtf +127.0.0.1 xn--lg-bja9b.wtf +``` + - Install [Pebble](https://github.com/letsencrypt/pebble): ```bash -go install github.com/letsencrypt/pebble/v2/cmd/pebble@v2.9.0 -go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@v2.9.0 +go install github.com/letsencrypt/pebble/v2/cmd/pebble@main +go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@main ``` - Launch tests: diff --git a/go.mod b/go.mod index b8e88428e..f770fe424 100644 --- a/go.mod +++ b/go.mod @@ -1,229 +1,190 @@ module github.com/go-acme/lego/v4 -go 1.24.0 +go 1.22 + +// github.com/exoscale/egoscale v1.19.0 => It is an error, please don't use it. require ( - cloud.google.com/go/compute/metadata v0.9.0 + cloud.google.com/go/compute/metadata v0.2.3 github.com/Azure/azure-sdk-for-go v68.0.0+incompatible - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 - github.com/Azure/go-autorest/autorest v0.11.30 - github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 - github.com/Azure/go-autorest/autorest/to v0.4.1 - github.com/BurntSushi/toml v1.6.0 - github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 - github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15 - github.com/alibabacloud-go/tea v1.4.0 - github.com/aliyun/credentials-go v1.4.7 - github.com/aws/aws-sdk-go-v2 v1.41.1 - github.com/aws/aws-sdk-go-v2/config v1.32.8 - github.com/aws/aws-sdk-go-v2/credentials v1.19.8 - github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11 - github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 - github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 - github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 - github.com/aziontech/azionapi-go-sdk v0.144.0 - github.com/baidubce/bce-sdk-go v0.9.260 - github.com/cenkalti/backoff/v5 v5.0.3 - github.com/dnsimple/dnsimple-go/v4 v4.0.0 - github.com/exoscale/egoscale/v3 v3.1.33 - github.com/go-acme/alidns-20150109/v4 v4.7.0 - github.com/go-acme/esa-20240910/v2 v2.48.0 - github.com/go-acme/jdcloud-sdk-go v1.64.0 - github.com/go-acme/tencentclouddnspod v1.3.24 - github.com/go-acme/tencentedgdeone v1.3.38 - github.com/go-jose/go-jose/v4 v4.1.3 - github.com/go-viper/mapstructure/v2 v2.5.0 - github.com/google/go-cmp v0.7.0 - github.com/google/go-querystring v1.2.0 - github.com/google/uuid v1.6.0 - github.com/gophercloud/gophercloud v1.14.1 + github.com/Azure/go-autorest/autorest v0.11.29 + github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 + github.com/Azure/go-autorest/autorest/to v0.4.0 + github.com/BurntSushi/toml v1.3.2 + github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 + github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 + github.com/aliyun/alibaba-cloud-sdk-go v1.62.712 + github.com/aws/aws-sdk-go-v2 v1.26.1 + github.com/aws/aws-sdk-go-v2/config v1.27.11 + github.com/aws/aws-sdk-go-v2/credentials v1.17.11 + github.com/aws/aws-sdk-go-v2/service/lightsail v1.37.0 + github.com/aws/aws-sdk-go-v2/service/route53 v1.40.4 + github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1 + github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 + github.com/cenkalti/backoff/v4 v4.3.0 + github.com/civo/civogo v0.3.11 + github.com/cloudflare/cloudflare-go v0.93.0 + github.com/cpu/goacmedns v0.1.1 + github.com/dnsimple/dnsimple-go v1.7.0 + github.com/exoscale/egoscale v0.102.3 + github.com/go-jose/go-jose/v4 v4.0.1 + github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 + github.com/google/go-querystring v1.1.0 + github.com/gophercloud/gophercloud v1.11.0 github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 - github.com/hashicorp/go-retryablehttp v0.7.8 - github.com/hashicorp/go-version v1.8.0 - github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187 + github.com/hashicorp/go-retryablehttp v0.7.5 github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df - github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 + github.com/infobloxopen/infoblox-go-client v1.1.1 github.com/labbsr0x/bindman-dns-webhook v1.0.2 - github.com/ldez/grignotin v0.10.1 - github.com/linode/linodego v1.65.0 + github.com/linode/linodego v1.28.0 github.com/liquidweb/liquidweb-go v1.6.4 github.com/mattn/go-isatty v0.0.20 - github.com/miekg/dns v1.1.72 + github.com/miekg/dns v1.1.58 github.com/mimuret/golang-iij-dpf v0.9.1 - github.com/namedotcom/go/v4 v4.0.2 - github.com/nrdcg/auroradns v1.2.0 - github.com/nrdcg/bunny-go v0.1.0 - github.com/nrdcg/desec v0.11.1 + github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 + github.com/nrdcg/auroradns v1.1.0 + github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 + github.com/nrdcg/desec v0.8.0 github.com/nrdcg/dnspod-go v0.4.0 - github.com/nrdcg/freemyip v0.3.0 - github.com/nrdcg/goacmedns v0.2.0 - github.com/nrdcg/goinwx v0.12.0 - github.com/nrdcg/mailinabox v0.3.0 - github.com/nrdcg/namesilo v0.5.0 + github.com/nrdcg/freemyip v0.2.0 + github.com/nrdcg/goinwx v0.10.0 + github.com/nrdcg/mailinabox v0.2.0 + github.com/nrdcg/namesilo v0.2.1 github.com/nrdcg/nodion v0.1.0 - github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 - github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 - github.com/nrdcg/porkbun v0.4.0 - github.com/nrdcg/vegadns v0.3.0 + github.com/nrdcg/porkbun v0.3.0 github.com/nzdjb/go-metaname v1.0.0 - github.com/ovh/go-ovh v1.9.0 - github.com/pquerna/otp v1.5.0 + github.com/oracle/oci-go-sdk/v65 v65.63.1 + github.com/ovh/go-ovh v1.5.1 + github.com/pquerna/otp v1.4.0 github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2 - github.com/regfish/regfish-dnsapi-go v0.1.1 - github.com/sacloud/api-client-go v0.3.3 - github.com/sacloud/iaas-api-go v1.23.1 - github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 - github.com/selectel/domains-go v1.1.0 - github.com/selectel/go-selvpcclient/v4 v4.1.0 - github.com/softlayer/softlayer-go v1.2.1 - github.com/stretchr/testify v1.11.1 - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48 - github.com/transip/gotransip/v6 v6.26.1 - github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 - github.com/urfave/cli/v2 v2.27.7 - github.com/vinyldns/go-vinyldns v0.9.17 - github.com/volcengine/volc-sdk-golang v1.0.237 - github.com/vultr/govultr/v3 v3.27.0 - github.com/yandex-cloud/go-genproto v0.54.0 - github.com/yandex-cloud/go-sdk/services/dns v0.0.36 - github.com/yandex-cloud/go-sdk/v2 v2.56.0 - golang.org/x/crypto v0.48.0 - golang.org/x/net v0.50.0 - golang.org/x/oauth2 v0.35.0 - golang.org/x/text v0.34.0 - golang.org/x/time v0.14.0 - google.golang.org/api v0.267.0 - gopkg.in/ns1/ns1-go.v2 v2.17.2 + github.com/sacloud/api-client-go v0.2.10 + github.com/sacloud/iaas-api-go v1.12.0 + github.com/scaleway/scaleway-sdk-go v1.0.0-beta.25 + github.com/selectel/domains-go v1.0.2 + github.com/selectel/go-selvpcclient/v3 v3.1.1 + github.com/softlayer/softlayer-go v1.1.3 + github.com/stretchr/testify v1.9.0 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.898 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898 + github.com/transip/gotransip/v6 v6.23.0 + github.com/ultradns/ultradns-go-sdk v1.6.1-20231103022937-8589b6a + github.com/urfave/cli/v2 v2.27.1 + github.com/vinyldns/go-vinyldns v0.9.16 + github.com/vultr/govultr/v2 v2.17.2 + github.com/yandex-cloud/go-genproto v0.0.0-20240318083951-4fe6125f286e + github.com/yandex-cloud/go-sdk v0.0.0-20240318084659-dfa50323a0b4 + golang.org/x/crypto v0.22.0 + golang.org/x/net v0.24.0 + golang.org/x/oauth2 v0.19.0 + golang.org/x/time v0.5.0 + google.golang.org/api v0.172.0 + gopkg.in/ns1/ns1-go.v2 v2.9.1 gopkg.in/yaml.v2 v2.4.0 - software.sslmate.com/src/go-pkcs12 v0.7.0 + software.sslmate.com/src/go-pkcs12 v0.4.0 ) require ( - cloud.google.com/go/auth v0.18.1 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute v1.24.0 // indirect github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect - github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect + github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect - github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect - github.com/alibabacloud-go/debug v1.0.1 // indirect - github.com/alibabacloud-go/openapi-util v0.1.1 // indirect - github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect - github.com/aws/smithy-go v1.24.0 // indirect - github.com/benbjohnson/clock v1.3.5 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.20.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 // indirect + github.com/aws/smithy-go v1.20.2 // indirect + github.com/benbjohnson/clock v1.3.0 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/clbanning/mxj/v2 v2.7.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/deepmap/oapi-codegen v1.9.1 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect - github.com/fatih/color v1.16.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-errors/errors v1.0.1 // indirect - github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.23.0 // indirect - github.com/go-resty/resty/v2 v2.17.1 // indirect - github.com/goccy/go-yaml v1.9.8 // indirect - github.com/gofrs/flock v0.13.0 // indirect + github.com/go-resty/resty/v2 v2.11.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/gofrs/flock v0.8.1 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect - github.com/golang-jwt/jwt/v4 v4.5.2 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect - github.com/googleapis/gax-go/v2 v2.17.0 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.3 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/labbsr0x/goh v1.0.1 // indirect - github.com/leodido/go-urn v1.4.0 // indirect github.com/liquidweb/liquidweb-cli v0.6.9 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.1.0 // indirect - github.com/peterhellberg/link v1.2.0 // indirect + github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sacloud/go-http v0.1.9 // indirect - github.com/sacloud/packages-go v0.0.12 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/shopspring/decimal v1.4.0 // indirect + github.com/sacloud/go-http v0.1.8 // indirect + github.com/sacloud/packages-go v0.0.10 // indirect + github.com/shopspring/decimal v1.3.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect + github.com/smartystreets/assertions v1.0.1 // indirect + github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect - github.com/sony/gobreaker v1.0.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.7.0 // indirect - github.com/spf13/pflag v1.0.7 // indirect - github.com/spf13/viper v1.18.2 // indirect + github.com/sony/gobreaker v0.5.0 // indirect + github.com/spf13/cast v1.3.1 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - github.com/tjfoc/gmsm v1.4.1 // indirect - github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect - github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect - go.mongodb.org/mongo-driver v1.13.1 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel v1.39.0 // indirect - go.opentelemetry.io/otel/metric v1.39.0 // indirect - go.opentelemetry.io/otel/trace v1.39.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/ratelimit v0.3.1 // indirect - go.uber.org/zap v1.27.0 // indirect - golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect - golang.org/x/mod v0.32.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/tools v0.41.0 // indirect - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect - google.golang.org/grpc v1.78.0 // indirect - google.golang.org/protobuf v1.36.11 // indirect - gopkg.in/ini.v1 v1.67.1 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + go.uber.org/ratelimit v0.3.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.20.0 // indirect + google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/grpc v1.63.1 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -retract v4.30.0 // Problem related to misuse of sycalls by aliyun/credentials-go diff --git a/go.sum b/go.sum index f5b87c9fe..277c8f8e4 100644 --- a/go.sum +++ b/go.sum @@ -5,223 +5,122 @@ cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6A cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= -cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= -cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= -cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= -cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= +cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYsMyFh9qoE= github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2 h1:FDif4R1+UUR+00q6wquyX90K7A8dN+R5E8GEadoP7sU= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2/go.mod h1:aiYBYui4BJ/BJCAIKs92XiPyQfTaBWqvHujDwKb6CBU= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 h1:LqbJ/WzJUwBf8UiaSzgX7aMclParm9/5Vgp+TY51uBQ= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 h1:yzrctSl9GMIQ5lHu7jc8olOsGjWDCsBpJhWqfGa/YIM= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0/go.mod h1:GE4m0rnnfwLGX0Y9A9A25Zx5N/90jneT5ABevqzhuFQ= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.2.0 h1:9Eih8XcEeQnFD0ntMlUDleKMzfeCeUfa+VbnDCI4AZs= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.2.0/go.mod h1:wGPyTi+aURdqPAGMZDQqnNs9IrShADF8w2WZb6bKeq0= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 h1:zLzoX5+W2l95UJoVwiyNS4dX8vHyQ6x2xRLoBBL9wMk= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1 h1:7CBQ+Ei8SP2c6ydQTGCCrS35bDxgTMfoP2miAwK++OU= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1/go.mod h1:c/wcGeGx5FUPbM/JltUYHZcKmigwyVLJlDq+4HdtXaw= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA= -github.com/Azure/go-autorest/autorest v0.11.30 h1:iaZ1RGz/ALZtN5eq4Nr1SOFSlf2E4pDI3Tcsl+dZPVE= -github.com/Azure/go-autorest/autorest v0.11.30/go.mod h1:t1kpPIOpIVX7annvothKvb0stsrXa37i7b+xpmBW8Fs= +github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= +github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw= +github.com/Azure/go-autorest/autorest v0.11.29/go.mod h1:ZtEzC4Jy2JDrZLxvWs8LrBWEBycl1hbT1eknI8MtfAs= github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= github.com/Azure/go-autorest/autorest/adal v0.9.22 h1:/GblQdIudfEM3AWWZ0mrYJQSd7JS4S/Mbzh6F0ov0Xc= github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 h1:Ov8avRZi2vmrE2JcXw+tu5K/yB41r7xK9GZDiBF7NdM= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.13/go.mod h1:5BAVfWLWXihP47vYrPuBKKf4cS0bXI+KM9Qx6ETDJYo= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 h1:wkAZRgT/pn8HhFyzfe9UnqOjJYqlembgCTi72Bm/xKk= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.12/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 h1:0W/yGmFdTIT77fvdlGZ0LMISoLHFJ7Tx4U0yeB+uFs4= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg= github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= -github.com/Azure/go-autorest/autorest/to v0.4.1 h1:CxNHBqdzTr7rLtdrtb5CMjJcDut+WNGCVv7OmS5+lTc= -github.com/Azure/go-autorest/autorest/to v0.4.1/go.mod h1:EtaofgU4zmtvn1zT2ARsjRFdq9vXx0YWtmElwL+GZ9M= +github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= +github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= -github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= -github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= -github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= -github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= -github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/sarama v1.30.1/go.mod h1:hGgx05L/DiW8XYBXeJdKIN6V2QUy2H6JqME5VT1NLRw= -github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae/go.mod h1:/cvHQkZ1fst0EmZnA5dFtiQdWCNCFYzb+uE2vqVgvx0= -github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 h1:xPMsUicZ3iosVPSIP7bW5EcGUzjiiMl1OYTe14y/R24= +github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 h1:h/33OxYLqBk0BYmEbSUy7MlvgQR/m1w1/7OJFKoPL1I= -github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0/go.mod h1:rvh3imDA6EaQi+oM/GQHkQAOHbXPKJ7EWJvfjuw141Q= +github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 h1:F1j7z+/DKEsYqZNoxC6wvfmaiDneLsQOFQmuq9NADSY= +github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2/go.mod h1:QlXr/TrICfQ/ANa76sLeQyhAJyNR9sEcfNuZBkY9jgY= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA= -github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo= -github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= -github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8= -github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g= -github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY= -github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI= -github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE= -github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8= -github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc= -github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc= -github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE= -github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.14/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE= -github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15 h1:Mubp9hXZMTPWZK+WxrR+kKOVFp4Q/PDZrIIM7ByXI9Y= -github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE= -github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg= -github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ= -github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo= -github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA= -github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY= -github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc= -github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg= -github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc= -github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q= -github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE= -github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= -github.com/alibabacloud-go/openapi-util v0.1.1 h1:ujGErJjG8ncRW6XtBBMphzHTvCxn4DjrVw4m04HsS28= -github.com/alibabacloud-go/openapi-util v0.1.1/go.mod h1:/UehBSE2cf1gYT43GV4E+RxTdLRzURImCYY0aRmlXpw= -github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg= -github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= -github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= -github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= -github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= -github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= -github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk= -github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg= -github.com/alibabacloud-go/tea v1.4.0 h1:MSKhu/kWLPX7mplWMngki8nNt+CyUZ+kfkzaR5VpMhA= -github.com/alibabacloud-go/tea v1.4.0/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg= -github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= -github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4= -github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= -github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0= -github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= -github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw= -github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0= -github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM= -github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= -github.com/aliyun/credentials-go v1.4.7 h1:T17dLqEtPUFvjDRRb5giVvLh6dFT8IcNFJJb7MeyCxw= -github.com/aliyun/credentials-go v1.4.7/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= +github.com/aliyun/alibaba-cloud-sdk-go v1.62.712 h1:lM7JnA9dEdDFH9XOgRNQMDTQnOjlLkDTNA7c0aWTQ30= +github.com/aliyun/alibaba-cloud-sdk-go v1.62.712/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= -github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= -github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= -github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= -github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= -github.com/aws/aws-sdk-go-v2/config v1.32.8 h1:iu+64gwDKEoKnyTQskSku72dAwggKI5sV6rNvgSMpMs= -github.com/aws/aws-sdk-go-v2/config v1.32.8/go.mod h1:MI2XvA+qDi3i9AJxX1E2fu730syEBzp/jnXrjxuHwgI= -github.com/aws/aws-sdk-go-v2/credentials v1.19.8 h1:Jp2JYH1lRT3KhX4mshHPvVYsR5qqRec3hGvEarNYoR0= -github.com/aws/aws-sdk-go-v2/credentials v1.19.8/go.mod h1:fZG9tuvyVfxknv1rKibIz3DobRaFw1Poe8IKtXB3XYY= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0= -github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g= -github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11 h1:VM5e5M39zRSs+aT0O9SoxHjUXqXxhbw3Yi0FdMQWPIc= -github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11/go.mod h1:0jvzYPIQGCpnY/dmdaotTk2JH4QuBlnW0oeyrcGLWJ4= -github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 h1:1jIdwWOulae7bBLIgB36OZ0DINACb1wxM6wdGlx4eHE= -github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1/go.mod h1:tE2zGlMIlxWv+7Otap7ctRp3qeKqtnja7DZguj3Vu/Y= -github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA= -github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 h1:0jbJeuEHlwKJ9PfXtpSFc4MF+WIWORdhN1n30ITZGFM= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= -github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= -github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= -github.com/aziontech/azionapi-go-sdk v0.144.0 h1:T+/w18o+FCiZsk3Z0ACBVVe7c/5EGLG15S3P8JfuPfo= -github.com/aziontech/azionapi-go-sdk v0.144.0/go.mod h1:OKxP/R0iVXnJJakYwMhh2BGAXnud8Ruy55Ak9ANuWoU= -github.com/baidubce/bce-sdk-go v0.9.260 h1:1v1+2GTP+NGK3L24rJ+bnoiTaDaIy2YoaUM+ot2GTcw= -github.com/baidubce/bce-sdk-go v0.9.260/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= -github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= +github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg= +github.com/aws/aws-sdk-go-v2/config v1.27.11 h1:f47rANd2LQEYHda2ddSCKYId18/8BhSRM4BULGmfgNA= +github.com/aws/aws-sdk-go-v2/config v1.27.11/go.mod h1:SMsV78RIOYdve1vf36z8LmnszlRWkwMQtomCAI0/mIE= +github.com/aws/aws-sdk-go-v2/credentials v1.17.11 h1:YuIB1dJNf1Re822rriUOTxopaHHvIq0l/pX3fwO+Tzs= +github.com/aws/aws-sdk-go-v2/credentials v1.17.11/go.mod h1:AQtFPsDH9bI2O+71anW6EKL+NcD7LG3dpKGMV4SShgo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5 h1:81KE7vaZzrl7yHBYHVEzYB8sypz11NMOZ40YlWvPxsU= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5/go.mod h1:LIt2rg7Mcgn09Ygbdh/RdIm0rQ+3BNkbP1gyVMFtRK0= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7 h1:ZMeFZ5yk+Ek+jNr1+uwCd2tG89t6oTS5yVWpa6yy2es= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7/go.mod h1:mxV05U+4JiHqIpGqqYXOHLPKUC6bDXC44bsUhNjOEwY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 h1:f9RyWNtS8oH7cZlbn+/JNPpjUk5+5fLd5lM9M0i49Ys= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5/go.mod h1:h5CoMZV2VF297/VLhRhO1WF+XYWOzXo+4HsObA4HjBQ= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.37.0 h1:lfVUMJEGXzi5l8jam/WXLNSn+vM/fpe2dmMYOdRiQ+k= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.37.0/go.mod h1:GSVUed6FJivX6v7Pgrk9iXuRa2NuCtT+nMWGxQHSAXQ= +github.com/aws/aws-sdk-go-v2/service/route53 v1.40.4 h1:ZZKiHm4cN8IDDZ2kh8DTk+YnYBjVsiFdwf5FwVs//IQ= +github.com/aws/aws-sdk-go-v2/service/route53 v1.40.4/go.mod h1:RTfjFUctf+Zyq8e4rgLXmz43+0kIoIXbENvrFtilumI= +github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1 h1:6cnno47Me9bRykw9AEv9zkXE+5or7jz8TsskTTccbgc= +github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1/go.mod h1:qmdkIIAC+GCLASF7R2whgNrJADz0QZPX+Seiw/i4S3o= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.5 h1:vN8hEbpRnL7+Hopy9dzmRle1xmDc7o8tmY0klsr175w= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.5/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 h1:Jux+gDDyi1Lruk+KHF91tK2KCuY61kzoCpvtvJJBtOE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 h1:cwIxeBttqPN3qkaAjcEcsh8NYr8n2HZPkcKgPAi1phU= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.6/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw= +github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= +github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -230,188 +129,141 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw= -github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= -github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= -github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= -github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= -github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= -github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/civo/civogo v0.3.11 h1:mON/fyrV946Sbk6paRtOSGsN+asCgCmHCgArf5xmGxM= +github.com/civo/civogo v0.3.11/go.mod h1:7+GeeFwc4AYTULaEshpT2vIcl3Qq8HPoxA17viX3l6g= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/cloudflare-go v0.93.0 h1:rV0eHb42NUewfK5qa2+LKAD4v4oFA0QGDABn/lMyF78= +github.com/cloudflare/cloudflare-go v0.93.0/go.mod h1:N1u1cLZ4lG6NeezGOWi7P6aq1DK2iVYg9ze7GZbUmZE= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpu/goacmedns v0.1.1 h1:DM3H2NiN2oam7QljgGY5ygy4yDXhK5Z4JUnqaugs2C4= +github.com/cpu/goacmedns v0.1.1/go.mod h1:MuaouqEhPAHxsbqjgnck5zeghuwBP1dLnPoobeGqugQ= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= -github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= +github.com/deepmap/oapi-codegen v1.9.1 h1:yHmEnA7jSTUMQgV+uN02WpZtwHnz2CBW3mZRIxr1vtI= +github.com/deepmap/oapi-codegen v1.9.1/go.mod h1:PLqNAhdedP8ttRpBBkzLKU3bp+Fpy+tTgeAMlztR2cw= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= -github.com/dnsimple/dnsimple-go/v4 v4.0.0 h1:nUCICZSyZDiiqimAAL+E8XL+0sKGks5VRki5S8XotRo= -github.com/dnsimple/dnsimple-go/v4 v4.0.0/go.mod h1:AXT2yfAFOntJx6iMeo1J/zKBw0ggXFYBt4e97dqqPnc= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/dnsimple/dnsimple-go v1.7.0 h1:JKu9xJtZ3SqOC+BuYgAWeab7+EEx0sz422vu8j611ZY= +github.com/dnsimple/dnsimple-go v1.7.0/go.mod h1:EKpuihlWizqYafSnQHGCd/gyvy3HkEQJ7ODB4KdV8T8= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/exoscale/egoscale/v3 v3.1.33 h1:5Lk/pwZ+K0sjNu9obS0VYPfhZQffRkvvO0BpdPoir4o= -github.com/exoscale/egoscale/v3 v3.1.33/go.mod h1:0iY8OxgHJCS5TKqDNhwOW95JBKCnBZl3YGU4Yt+NqkU= +github.com/exoscale/egoscale v0.102.3 h1:DYqN2ipoLKpiFoprRGQkp2av/Ze7sUYYlGhi1N62tfY= +github.com/exoscale/egoscale v0.102.3/go.mod h1:RPf2Gah6up+6kAEayHTQwqapzXlm93f0VQas/UEGU5c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo= -github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= -github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/getkin/kin-openapi v0.87.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-acme/alidns-20150109/v4 v4.7.0 h1:PqJ/wR0JTpL4v0Owu1uM7bPQ1Yww0eQLAuuSdLjjQaQ= -github.com/go-acme/alidns-20150109/v4 v4.7.0/go.mod h1:btQvB6xZoN6ykKB74cPhiR+uvhrEE2AFVXm6RDmCHm0= -github.com/go-acme/esa-20240910/v2 v2.48.0 h1:muSDyhjDTejxUGe3FTthCPCqRaEdYY9cG3N/AmU52Lc= -github.com/go-acme/esa-20240910/v2 v2.48.0/go.mod h1:shPb6hzc1rJL15IJBY8HQ4GZk4E8RC52+52twutEwIg= -github.com/go-acme/jdcloud-sdk-go v1.64.0 h1:AW9j5khk8tRYbpBJPxKmqdwIqgLs2Fz3HUK3hn2YXjs= -github.com/go-acme/jdcloud-sdk-go v1.64.0/go.mod h1:qc/m8HNX1Zgd7GAv2DSEinup8fwy3Ted3/VVx7LB5bU= -github.com/go-acme/tencentclouddnspod v1.3.24 h1:uCSiOW1EJttcnOON+MVVyVDJguFL/Q4NIGkq1CrT9p8= -github.com/go-acme/tencentclouddnspod v1.3.24/go.mod h1:RKcB2wSoZncjBA0OEFj59s1ko1XDy+ZsAtk+9uMxUF0= -github.com/go-acme/tencentedgdeone v1.3.38 h1:5YsVl0H4A+cwtiUqR1eZbKFdr4OWfYp2KYJopifzKyQ= -github.com/go-acme/tencentedgdeone v1.3.38/go.mod h1:yyjTKVmGpMtFv5HqGODqehHnZJ4KWAbG6dAiwWDgCDY= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= +github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= +github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es= -github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= -github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= -github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4= -github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= +github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8= +github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= -github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA= github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw= -github.com/goccy/go-yaml v1.9.8 h1:5gMyLUeU1/6zl+WFfR1hN7D2kf+1/eRGa7DFtToiBvQ= -github.com/goccy/go-yaml v1.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= -github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= +github.com/goccy/go-json v0.7.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= -github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -424,183 +276,128 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= -github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= -github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= -github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= -github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= +github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= +github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= github.com/gophercloud/gophercloud v1.3.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= -github.com/gophercloud/gophercloud v1.14.1 h1:DTCNaTVGl8/cFu58O1JwWgis9gtISAFONqpMKNg/Vpw= -github.com/gophercloud/gophercloud v1.14.1/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= +github.com/gophercloud/gophercloud v1.11.0 h1:ls0O747DIq1D8SUHc7r2vI8BFbMLeLFuENaAIfEx7OM= +github.com/gophercloud/gophercloud v1.11.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 h1:sH7xkTfYzxIEgzq1tDHIMKRh1vThOEOGNsettdEeLbE= github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56/go.mod h1:VSalo4adEk+3sNkmVJLnhHoOyOYYS8sTWLG4mv5BKto= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= -github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= +github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= -github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= +github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= -github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187 h1:J+U6+eUjIsBhefolFdZW5hQNJbkMj+7msxZrv56Cg2g= -github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI= -github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df h1:MZf03xP9WdakyXhOWuAD5uPK3wHh96wCsqe3hCMKh8E= github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 h1:AKsihjFT/t6Y0keEv3p59DACcOuh0inWXdUB0ZOzYH0= -github.com/infobloxopen/infoblox-go-client/v2 v2.10.0/go.mod h1:NeNJpz09efw/edzqkVivGv1bWqBXTomqYBRFbP+XBqg= +github.com/infobloxopen/infoblox-go-client v1.1.1 h1:728A6LbLjptj/7kZjHyIxQnm768PWHfGFm0HH8FnbtU= +github.com/infobloxopen/infoblox-go-client v1.1.1/go.mod h1:BXiw7S2b9qJoM8MS40vfgCNB2NLHGusk1DtO16BD9zI= github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= -github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A= -github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0= -github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= -github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= -github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= -github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= -github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= -github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= +github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU= -github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= -github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= -github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -611,13 +408,19 @@ github.com/labbsr0x/bindman-dns-webhook v1.0.2 h1:I7ITbmQPAVwrDdhd6dHKi+MYJTJqPC github.com/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA= github.com/labbsr0x/goh v1.0.1 h1:97aBJkDjpyBZGPbQuOK5/gHcSFbcr5aRsq3RSRJFpPk= github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w= -github.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o= -github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas= +github.com/labstack/echo/v4 v4.6.3/go.mod h1:Hk5OiHj0kDqmFq7aHe7eDqI7CUhuCrfpupQtLGGLm7A= +github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7k= -github.com/linode/linodego v1.65.0/go.mod h1:tOFiTErdjkbVnV+4S0+NmIE9dqqZUEM2HsJaGu8wMh8= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= +github.com/lestrrat-go/codegen v1.0.2/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM= +github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE= +github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= +github.com/lestrrat-go/jwx v1.2.7/go.mod h1:bw24IXWbavc0R2RsOtpXL7RtMyP589yZ1+L7kd09ZGA= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/linode/linodego v1.28.0 h1:lzxxJebsYg5cCWRNDLyL2StW3sfMyAwf/FYfxFjFrlk= +github.com/linode/linodego v1.28.0/go.mod h1:5oAsx+uinHtVo6U77nXXXtox7MWzUW6aEkTOKXxA9uo= github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs= github.com/liquidweb/liquidweb-cli v0.6.9 h1:acbIvdRauiwbxIsOCEMXGwF75aSJDbDiyAWPjVnwoYM= github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ= @@ -625,22 +428,21 @@ github.com/liquidweb/liquidweb-go v1.6.4 h1:6S0m3hHSpiLqGD7AFSb7lH/W/qr1wx+tKil9 github.com/liquidweb/liquidweb-go v1.6.4/go.mod h1:B934JPIIcdA+uTq2Nz5PgOtG6CuCaEvQKe/Ge/5GgZ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= @@ -650,29 +452,22 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= -github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= +github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= github.com/mimuret/golang-iij-dpf v0.9.1 h1:Gj6EhHJkOhr+q2RnvRPJsPMcjuVnWPSccEHyoEehU34= github.com/mimuret/golang-iij-dpf v0.9.1/go.mod h1:sl9KyOkESib9+KRD3HaGpgi1xk7eoN2+d96LCLsME2M= -github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= -github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= -github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -682,47 +477,31 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/namedotcom/go/v4 v4.0.2 h1:4gNkPaPRG/2tqFNUUof7jAVsA6vDutFutEOd7ivnDwA= -github.com/namedotcom/go/v4 v4.0.2/go.mod h1:J6sVueHMb0qbarPgdhrzEVhEaYp+R1SCaTGl2s6/J1Q= -github.com/nats-io/jwt v1.2.2/go.mod h1:/xX356yQA6LuXI9xWW7mZNpxgF2mBmGecH+Fj34sP5Q= -github.com/nats-io/jwt/v2 v2.0.3/go.mod h1:VRP+deawSXyhNjXmxPCHskrR6Mq50BqpEI5SEcNiGlY= -github.com/nats-io/nats-server/v2 v2.5.0/go.mod h1:Kj86UtrXAL6LwYRA6H4RqzkHhK0Vcv2ZnKD5WbQ1t3g= -github.com/nats-io/nats.go v1.12.1/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= -github.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s= -github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAAxrQxRHEu7FkapwTuI2WmL1rw4g= +github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nrdcg/auroradns v1.2.0 h1:Jg407vTdXZvZKsART9CNWMp8rQOyhBk04q0MsOf0YR4= -github.com/nrdcg/auroradns v1.2.0/go.mod h1:hnByA4Z7MOmV4EPRw5eOmEaNRFavcCIz6kONpNxp9LI= -github.com/nrdcg/bunny-go v0.1.0 h1:GAHTRpHaG/TxfLZlqoJ8OJFzw8rI74+jOTkzxWh0uHA= -github.com/nrdcg/bunny-go v0.1.0/go.mod h1:u+C9dgsspgtWVaAz6QkyV17s9fxD8viwwKoxb9XMz1A= -github.com/nrdcg/desec v0.11.1 h1:ilpKmCr4gGsLcyq3RHfHNmlRzm9fzT2XbWxoVaUCS0s= -github.com/nrdcg/desec v0.11.1/go.mod h1:2LuxHlOcwML/7cntu0eimONmA1U+ZxFDAonoSXr4igQ= +github.com/nrdcg/auroradns v1.1.0 h1:KekGh8kmf2MNwqZVVYo/fw/ZONt8QMEmbMFOeljteWo= +github.com/nrdcg/auroradns v1.1.0/go.mod h1:O7tViUZbAcnykVnrGkXzIJTHoQCHcgalgAe6X1mzHfk= +github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 h1:ouZ2JWDl8IW5k1qugYbmpbmW8hn85Ig6buSMBRlz3KI= +github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3/go.mod h1:ZwadWt7mVhMHMbAQ1w8IhDqtWO3eWqWq72W7trnaiE8= +github.com/nrdcg/desec v0.8.0 h1:FJbRWUAluTCUi9nHFnhqPhLSIHiNnB9elZVWYgFtIqA= +github.com/nrdcg/desec v0.8.0/go.mod h1:BsnYPtSlBttJL3Gyzv0kDH7zkk60obwThlnqiiKzn+o= github.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U= github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ= -github.com/nrdcg/freemyip v0.3.0 h1:0D2rXgvLwe2RRaVIjyUcQ4S26+cIS2iFwnhzDsEuuwc= -github.com/nrdcg/freemyip v0.3.0/go.mod h1:c1PscDvA0ukBF0dwelU/IwOakNKnVxetpAQ863RMJoM= -github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0= -github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg= -github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4= -github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0= -github.com/nrdcg/mailinabox v0.3.0 h1:PHkC1elKXKAjEvdx2HHFMgcEGZFqudAl7aU3L2JDhM4= -github.com/nrdcg/mailinabox v0.3.0/go.mod h1:1eFIGcM4lI+AfFOUpbs548SFGz1ZWoMOGbECBmkghw4= -github.com/nrdcg/namesilo v0.5.0 h1:6QNxT/XxE+f5B+7QlfWorthNzOzcGlBLRQxqi6YeBrE= -github.com/nrdcg/namesilo v0.5.0/go.mod h1:4UkwlwQfDt74kSGmhLaDylnBrD94IfflnpoEaj6T2qw= +github.com/nrdcg/freemyip v0.2.0 h1:/GscavT4GVqAY13HExl5UyoB4wlchv6Cg5NYDGsUoJ8= +github.com/nrdcg/freemyip v0.2.0/go.mod h1:HjF0Yz0lSb37HD2ihIyGz9esyGcxbCrrGFLPpKevbx4= +github.com/nrdcg/goinwx v0.10.0 h1:6W630bjDxQD6OuXKqrFRYVpTt0G/9GXXm3CeOrN0zJM= +github.com/nrdcg/goinwx v0.10.0/go.mod h1:mnMSTi7CXBu2io4DzdOBoGFA1XclD0sEPWJaDhNgkA4= +github.com/nrdcg/mailinabox v0.2.0 h1:IKq8mfKiVwNW2hQii/ng1dJ4yYMMv3HAP3fMFIq2CFk= +github.com/nrdcg/mailinabox v0.2.0/go.mod h1:0yxqeYOiGyxAu7Sb94eMxHPIOsPYXAjTeA9ZhePhGnc= +github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg= +github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw= github.com/nrdcg/nodion v0.1.0 h1:zLKaqTn2X0aDuBHHfyA1zFgeZfiCpmu/O9DM73okavw= github.com/nrdcg/nodion v0.1.0/go.mod h1:inbuh3neCtIWlMPZHtEpe43TmRXxHV6+hk97iCZicms= -github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 h1:OWijzl3nHUApvTivl+3+78dbBwmyEHOnb+W9m6ixGbk= -github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8= -github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 h1:9LsjN/zaIN7H8JE61NHpbWhxF0UGY96+kMlk3g8OvGU= -github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2/go.mod h1:32vZH06TuwZSn+IDMO1qcDvC2vHVlzUALCwXGWPA+dc= -github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw= -github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54= -github.com/nrdcg/vegadns v0.3.0 h1:11FQMw7xVIRUWO9o5+Z/5YZhmPWlm4oxUUH3F6EVqQU= -github.com/nrdcg/vegadns v0.3.0/go.mod h1:NqSyRKZuJlAsv8VI/7rSubfPXN68NwaJ0aG9KxQVFVo= +github.com/nrdcg/porkbun v0.3.0 h1:jnRV7j2zd3hmh+tSDOGetJyy3+WklaMxbs7HtTTmWMs= +github.com/nrdcg/porkbun v0.3.0/go.mod h1:jh1DKz96jGHW+NCdG3AmTbbnQeBlNUz1KeSgeN/cBVw= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -730,165 +509,123 @@ github.com/nzdjb/go-metaname v1.0.0 h1:sNASlZC1RM3nSudtBTE1a3ZVTDyTpjqI5WXRPrdZ9 github.com/nzdjb/go-metaname v1.0.0/go.mod h1:0GR0LshZax1Lz4VrOrfNSE4dGvTp7HGjiemdczXT2H4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0= -github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= -github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= -github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= -github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= -github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= -github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE= -github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE= -github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= +github.com/oracle/oci-go-sdk/v65 v65.63.1 h1:dYL7sk9L1+C9LCmoq+zjPMNteuJJfk54YExq/4pV9xQ= +github.com/oracle/oci-go-sdk/v65 v65.63.1/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0= +github.com/ovh/go-ovh v1.5.1 h1:P8O+7H+NQuFK9P/j4sFW5C0fvSS2DnHYGPwdVCp45wI= +github.com/ovh/go-ovh v1.5.1/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= -github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM= -github.com/peterhellberg/link v1.2.0 h1:UA5pg3Gp/E0F2WdX7GERiNrPQrM1K6CVJUUWfHa4t6c= -github.com/peterhellberg/link v1.2.0/go.mod h1:gYfAh+oJgQu2SrZHg5hROVRQe1ICoK0/HHJTcE0edxc= -github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= -github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= -github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= +github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2 h1:dq90+d51/hQRaHEqRAsQ1rE/pC1GUS4sc2rCbbFsAIY= github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA= -github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/regfish/regfish-dnsapi-go v0.1.1 h1:TJFtbePHkd47q5GZwYl1h3DIYXmoxdLjW/SBsPtB5IE= -github.com/regfish/regfish-dnsapi-go v0.1.1/go.mod h1:ubIgXSfqarSnl3XHSn8hIFwFF3h0yrq0ZiWD93Y2VjY= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sacloud/api-client-go v0.3.3 h1:ZpSAyGpITA8UFO3Hq4qMHZLGuNI1FgxAxo4sqBnCKDs= -github.com/sacloud/api-client-go v0.3.3/go.mod h1:0p3ukcWYXRCc2AUWTl1aA+3sXLvurvvDqhRaLZRLBwo= -github.com/sacloud/go-http v0.1.9 h1:Xa5PY8/pb7XWhwG9nAeXSrYXPbtfBWqawgzxD5co3VE= -github.com/sacloud/go-http v0.1.9/go.mod h1:DpDG+MSyxYaBwPJ7l3aKLMzwYdTVtC5Bo63HActcgoE= -github.com/sacloud/iaas-api-go v1.23.1 h1:rjYG0vVoxWyETiwc7R8YdD7CIzs9vVNEOzu7w6dgGzc= -github.com/sacloud/iaas-api-go v1.23.1/go.mod h1:EGIHOWRB9azOv7HPCVM8WpOEl28WIV9TNRbnEVg+Q3U= -github.com/sacloud/packages-go v0.0.12 h1:MKeZNN3FQn1heqUSRBrbZw89YusZA1n4kammjMFZYvQ= -github.com/sacloud/packages-go v0.0.12/go.mod h1:XNF5MCTWcHo9NiqWnYctVbASSSZR3ZOmmQORIzcurJ8= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM= +github.com/sacloud/api-client-go v0.2.10 h1:+rv3jDohD+pkdYwOTBiB+jZsM0xK3AxadXRzhp3q66c= +github.com/sacloud/api-client-go v0.2.10/go.mod h1:Jj3CTy2+O4bcMedVDXlbHuqqche85HEPuVXoQFhLaRc= +github.com/sacloud/go-http v0.1.8 h1:ynreWA/vnM8G2ksbMlmefBHsXURKPz49qlPRqQ9IQdw= +github.com/sacloud/go-http v0.1.8/go.mod h1:7TL7TN1fnPKHsMifIqURDkGujnKViCgEz5Ei/LQdFK8= +github.com/sacloud/iaas-api-go v1.12.0 h1:kqXFn3HzCiawlX6hVJb1GVqcSJqcmiGHB4Zp14sxiI8= +github.com/sacloud/iaas-api-go v1.12.0/go.mod h1:SZLXeWOdXk3WReIS557sbU1gkOgrE4rseIBQV1B3b7o= +github.com/sacloud/packages-go v0.0.10 h1:UiQGjy8LretewkRhsuna1TBM9Vz/l9FoYpQx+D+AOck= +github.com/sacloud/packages-go v0.0.10/go.mod h1:f8QITBh9z4IZc4yE9j21Q8b0sXEMwRlRmhhjWeDVTYs= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.25 h1:/8rfZAdFfafRXOgz+ZpMZZWZ5pYggCY9t7e/BvjaBHM= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.25/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/selectel/domains-go v1.1.0 h1:futG50J43ALLKQAnZk9H9yOtLGnSUh7c5hSvuC5gSHo= -github.com/selectel/domains-go v1.1.0/go.mod h1:SugRKfq4sTpnOHquslCpzda72wV8u0cMBHx0C0l+bzA= -github.com/selectel/go-selvpcclient/v4 v4.1.0 h1:22lBp+rzg9g2MP4iiGhpVAcCt0kMv7I7uV1W3taLSvQ= -github.com/selectel/go-selvpcclient/v4 v4.1.0/go.mod h1:eFhL1KUW159KOJVeGO7k/Uxl0TYd/sBkWXjuF5WxmYk= -github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= -github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/selectel/domains-go v1.0.2 h1:Si6iGaMnTFJxwiJVI50DOdZnwcxc87kqaWrVQYW0a4U= +github.com/selectel/domains-go v1.0.2/go.mod h1:SugRKfq4sTpnOHquslCpzda72wV8u0cMBHx0C0l+bzA= +github.com/selectel/go-selvpcclient/v3 v3.1.1 h1:C1q2LqqosiapoLpnGITGmysg0YCSQYDo2Gh69CioevM= +github.com/selectel/go-selvpcclient/v3 v3.1.1/go.mod h1:NM7IXhh1IzqZ88DOw1Qc5Ez3tULLViXo95l5+rKPuyQ= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w= +github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= +github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 h1:hp2CYQUINdZMHdvTdXtPOY2ainKl4IoMcpAXEf2xj3Q= +github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/softlayer/softlayer-go v1.2.1 h1:8ucHxn5laVsVPb0/aMGnr6tOMt1I9BgEtU5mn70OGKw= -github.com/softlayer/softlayer-go v1.2.1/go.mod h1:Gz9/ktcmB7Z8EJlu+QEJJpkv8lAmnhYdB9Tc6gedjmo= +github.com/smartystreets/gunit v1.0.4 h1:tpTjnuH7MLlqhoD21vRoMZbMIi5GmBsAJDFyF67GhZA= +github.com/smartystreets/gunit v1.0.4/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ= +github.com/softlayer/softlayer-go v1.1.3 h1:dfFzt5eOKIAyB/b78fHMyDu5ICx0ZtxL9NRhBlf831A= +github.com/softlayer/softlayer-go v1.1.3/go.mod h1:Pc7F57OgUKaAam7TtpqkUeqL7QyKknfiUI4R49h41/U= github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e h1:3OgWYFw7jxCZPcvAg+4R8A50GZ+CCkARF10lxu2qDsQ= github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e/go.mod h1:fKZCUVdirrxrBpwd9wb+lSoVixvpwAu8eHzbQB2tums= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= -github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ= -github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= +github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= -github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= -github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= -github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -901,142 +638,90 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.24/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.38/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48 h1:bCs+z6dxRaHWm/C1D/XkSOcCZ0+W2+/6HmIXjpAj+fY= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= -github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= -github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.898 h1:ERwcXqhc94L9cFxtiI0pvt7IJtlHl/p/Jayl3mLw+ms= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.898/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898 h1:LoYv5u+gUoFpU/AmIuTRG/2KiEkdm9gCC0dTvk8WITQ= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898/go.mod h1:c1j6YQ+vCbeA8kJ59Im4UnMd1GxovlpPBDhGZoewfn8= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/transip/gotransip/v6 v6.26.1 h1:MeqIjkTBBsZwWAK6giZyMkqLmKMclVHEuTNmoBdx4MA= -github.com/transip/gotransip/v6 v6.26.1/go.mod h1:x0/RWGRK/zob817O3tfO2xhFoP1vu8YOHORx6Jpk80s= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 h1:/VaznPrb/b68e3iMvkr27fU7JqPKU4j7tIITZnjQX1k= -github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419/go.mod h1:QN0/PdenvYWB0GRMz6JJbPeZz2Lph2iys1p8AFVHm2c= -github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= -github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= -github.com/vinyldns/go-vinyldns v0.9.17 h1:hfPZfCaxcRBX6Gsgl42rLCeoal58/BH8kkvJShzjjdI= -github.com/vinyldns/go-vinyldns v0.9.17/go.mod h1:pwWhE9K/leGDOIduVhRGvQ3ecVMHWRfEnKYUTEU3gB4= -github.com/volcengine/volc-sdk-golang v1.0.237 h1:hpLKiS2BwDcSBtZWSz034foCbd0h3FrHTKlUMqHIdc4= -github.com/volcengine/volc-sdk-golang v1.0.237/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM= -github.com/vultr/govultr/v3 v3.27.0 h1:J8etMyu/Jh5+idMsu2YZpOWmDXXHeW4VZnkYXmJYHx8= -github.com/vultr/govultr/v3 v3.27.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= -github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= -github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= -github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/transip/gotransip/v6 v6.23.0 h1:PsTdjortrEZ8IFFifEryzjVjOy9SgK4ahlnhKBBIQgA= +github.com/transip/gotransip/v6 v6.23.0/go.mod h1:nzv9eN2tdsUrm5nG5ZX6AugYIU4qgsMwIn2c0EZLk8c= +github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= +github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= +github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= +github.com/ultradns/ultradns-go-sdk v1.6.1-20231103022937-8589b6a h1:w4PK5/N9kq8PfNxBv8a5t1bqlYRrVT7XzT7iTPTtiPk= +github.com/ultradns/ultradns-go-sdk v1.6.1-20231103022937-8589b6a/go.mod h1:Xwz7o+ExFtxR/i0aJDnTXuiccQJlOxDgNe6FsZC4TzQ= +github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= +github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ= +github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q= +github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs= +github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= -github.com/yandex-cloud/go-genproto v0.54.0 h1:LjEwDPBAtF39HvcPQe8I+ImCnFasCPCOVh2b2Sr2eAg= -github.com/yandex-cloud/go-genproto v0.54.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo= -github.com/yandex-cloud/go-sdk/services/dns v0.0.36 h1:sD622+baDvJ2ujhCfoFsCH0XeNsaZNW6loRqvRavjtE= -github.com/yandex-cloud/go-sdk/services/dns v0.0.36/go.mod h1:Hh7IKJxULaRzmyM19lQZw+yUDyMM8M3Qrk1LbWqhCkc= -github.com/yandex-cloud/go-sdk/v2 v2.56.0 h1:rihPAZbPbHU/BKTLuT64nU1uhbBrO20HhdlLR3Hyoz0= -github.com/yandex-cloud/go-sdk/v2 v2.56.0/go.mod h1:jzVBQgamNHoiDsmjog2dPZHMXuGZqmxf/epH+Qb7Emc= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= -github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= -github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yandex-cloud/go-genproto v0.0.0-20240318083951-4fe6125f286e h1:jLIqA7M9qY31g/Nw/5htVD0DFbxmLnlFZcHKJiG3osI= +github.com/yandex-cloud/go-genproto v0.0.0-20240318083951-4fe6125f286e/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE= +github.com/yandex-cloud/go-sdk v0.0.0-20240318084659-dfa50323a0b4 h1:wtzLQJmghkSUb1YkeFphIh7ST7NNVDaVOJZSAJcjMdw= +github.com/yandex-cloud/go-sdk v0.0.0-20240318084659-dfa50323a0b4/go.mod h1:9d1MV6u4lK715YXnZceKqhP4L0bKBKmv4mSLnVSjJaM= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= -go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk= -go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= -go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= -go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= -go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= -go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= -go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= -go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0= -go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk= +go.uber.org/ratelimit v0.3.0 h1:IdZd9wqvFXnvLvSEBo0KPcGfkoBGNkpTHlrE3Rcjkjw= +go.uber.org/ratelimit v0.3.0/go.mod h1:So5LG7CV1zWpY1sHe+DXTJqQvOx+FFPFaAs2SnoyBaI= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1045,13 +730,6 @@ golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20241210194714-1829a127f884 h1:Y/Mj/94zIQQGHVSv1tTtQBDaQaJe62U9bkDZKKyhPCU= -golang.org/x/exp v0.0.0-20241210194714-1829a127f884/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -1062,26 +740,17 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1098,75 +767,42 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= -golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= +golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1174,7 +810,6 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1184,93 +819,51 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -1279,25 +872,19 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1315,58 +902,24 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= +golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= @@ -1374,25 +927,12 @@ google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE= -google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0= +google.golang.org/api v0.172.0 h1:/1OcMZGPmW1rX2LCu2CmGUD1KXK1+pfzxotxyRUCCdk= +google.golang.org/api v0.172.0/go.mod h1:+fJZq6QXWfa9pXhnIzsjx4yI22d4aI9ZpLb58gvXjis= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -1402,54 +942,28 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= -google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto v0.0.0-20211021150943-2b146023228c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= +google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 h1:rIo7ocm2roD9DcFIX67Ym8icoGCKSARAiPljFhh5suQ= +google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= +google.golang.org/grpc v1.63.1 h1:pNClQmvdlyNUiwFETOux/PYqfhmA7BrswEdGRnib1fA= +google.golang.org/grpc v1.63.1/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1458,14 +972,12 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1475,32 +987,30 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/h2non/gock.v1 v1.0.15 h1:SzLqcIlb/fDfg7UvukMpNcWsu7sI5tWwL+KCATZqks0= +gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= -gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= -gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/ns1/ns1-go.v2 v2.17.2 h1:x8YKHqCJWkC/hddfUhw7FRqTG0x3fr/0ZnWYN+i4THs= -gopkg.in/ns1/ns1-go.v2 v2.17.2/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc= +gopkg.in/ns1/ns1-go.v2 v2.9.1 h1:3/QYzUazRCSE49d3sh1Q+X7IrDp/I7OqR/M7dKA0Oks= +gopkg.in/ns1/ns1-go.v2 v2.9.1/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1508,12 +1018,7 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= -software.sslmate.com/src/go-pkcs12 v0.7.0 h1:Db8W44cB54TWD7stUFFSWxdfpdn6fZVcDl0w3R4RVM0= -software.sslmate.com/src/go-pkcs12 v0.7.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= +software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= +software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/internal/dns/descriptors/descriptors.go b/internal/dns/descriptors/descriptors.go deleted file mode 100644 index 838c454a7..000000000 --- a/internal/dns/descriptors/descriptors.go +++ /dev/null @@ -1,76 +0,0 @@ -package descriptors - -import ( - "os" - "path/filepath" - - "github.com/BurntSushi/toml" -) - -type Providers struct { - Providers []Provider -} - -type Provider struct { - Name string // Real name of the DNS provider - Code string // DNS code - Aliases []string // DNS code aliases (for compatibility/deprecation) - Since string // First lego version - URL string // DNS provider URL - Description string // Provider summary - Example string // CLI example - Configuration *Configuration // Environment variables - Links *Links // Links - Additional string // Extra documentation - GeneratedFrom string // Source file -} - -type Configuration struct { - Credentials map[string]string - Additional map[string]string -} - -type Links struct { - API string - GoClient string -} - -// GetProviderInformation extract provider information from TOML description files. -func GetProviderInformation(root string) (*Providers, error) { - models := &Providers{} - - err := filepath.Walk(filepath.Join(root, "providers", "dns"), walker(root, models)) - if err != nil { - return nil, err - } - - return models, nil -} - -func walker(root string, prs *Providers) func(string, os.FileInfo, error) error { - return func(path string, _ os.FileInfo, err error) error { - if err != nil { - return err - } - - if filepath.Ext(path) != ".toml" { - return nil - } - - m := Provider{} - - m.GeneratedFrom, err = filepath.Rel(root, path) - if err != nil { - return err - } - - _, err = toml.DecodeFile(path, &m) - if err != nil { - return err - } - - prs.Providers = append(prs.Providers, m) - - return nil - } -} diff --git a/internal/dns/docs/generator.go b/internal/dns/docs/generator.go deleted file mode 100644 index 9355d0d1b..000000000 --- a/internal/dns/docs/generator.go +++ /dev/null @@ -1,232 +0,0 @@ -package main - -//go:generate go run . - -import ( - "bufio" - "bytes" - "embed" - "errors" - "fmt" - "go/format" - html "html/template" - "log" - "os" - "path/filepath" - "slices" - "strings" - "text/template" - - "github.com/go-acme/lego/v4/internal/dns/descriptors" -) - -//go:embed templates -var templateFS embed.FS - -const ( - root = "../../../" - - cliOutput = root + "cmd/zz_gen_cmd_dnshelp.go" - docOutput = root + "docs/content/dns" - readmePath = root + "README.md" -) - -const ( - mdTemplate = "templates/dns.md.tmpl" - cliTemplate = "templates/dns.go.tmpl" - readmeTemplate = "templates/readme.md.tmpl" -) - -const ( - startLine = "" - endLine = "" -) - -func main() { - models, err := descriptors.GetProviderInformation(root) - if err != nil { - log.Fatal(err) - } - - err = cleanDocumentation() - if err != nil { - log.Fatal(err) - } - - for _, m := range models.Providers { - // generate documentation - err = generateDocumentation(m) - if err != nil { - log.Fatal(err) - } - } - - // generate CLI help - err = generateCLIHelp(models) - if err != nil { - log.Fatal(err) - } - - // generate README.md - err = generateReadMe(models) - if err != nil { - log.Fatal(err) - } - - fmt.Printf("Documentation for %d DNS providers has been generated.\n", len(models.Providers)+1) -} - -func cleanDocumentation() error { - paths, err := filepath.Glob(filepath.Join(docOutput, "zz_gen_*.md")) - if err != nil { - return err - } - - for _, p := range paths { - err = os.RemoveAll(p) - if err != nil { - return err - } - } - - return nil -} - -func generateDocumentation(m descriptors.Provider) error { - filename := filepath.Join(docOutput, "zz_gen_"+m.Code+".md") - - file, err := os.Create(filename) - if err != nil { - return err - } - - defer func() { _ = file.Close() }() - - return template.Must(template.ParseFS(templateFS, mdTemplate)).Execute(file, m) -} - -func generateCLIHelp(models *descriptors.Providers) error { - filename := filepath.Clean(cliOutput) - - file, err := os.Create(filename) - if err != nil { - return err - } - - defer func() { _ = file.Close() }() - - b := &bytes.Buffer{} - - err = template.Must( - template.New(filepath.Base(cliTemplate)).Funcs(map[string]any{ - "safe": func(src string) string { - return strings.ReplaceAll(src, "`", "'") - }, - }).ParseFS(templateFS, cliTemplate), - ).Execute(b, models) - if err != nil { - return err - } - - // gofmt - source, err := format.Source(b.Bytes()) - if err != nil { - return err - } - - _, err = file.Write(source) - - return err -} - -func generateReadMe(models *descriptors.Providers) error { - tpl := html.Must(html.New(filepath.Base(readmeTemplate)).ParseFS(templateFS, readmeTemplate)) - providers := orderProviders(models) - - file, err := os.Open(readmePath) - if err != nil { - return err - } - - defer func() { _ = file.Close() }() - - var skip bool - - buffer := bytes.NewBufferString("") - - fileScanner := bufio.NewScanner(file) - for fileScanner.Scan() { - text := fileScanner.Text() - - if text == startLine { - _, _ = fmt.Fprintln(buffer, text) - if err = tpl.Execute(buffer, providers); err != nil { - return err - } - - skip = true - } - - if text == endLine { - skip = false - } - - if skip { - continue - } - - _, _ = fmt.Fprintln(buffer, text) - } - - if fileScanner.Err() != nil { - return fileScanner.Err() - } - - if skip { - return errors.New("missing end tag") - } - - return os.WriteFile(readmePath, buffer.Bytes(), 0o666) -} - -func orderProviders(models *descriptors.Providers) [][]descriptors.Provider { - const nbCol = 4 - - slices.SortFunc(models.Providers, func(a, b descriptors.Provider) int { - return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name)) - }) - - var ( - matrix [][]descriptors.Provider - row []descriptors.Provider - ) - - for i, p := range models.Providers { - switch { - case len(row) == nbCol: - matrix = append(matrix, row) - row = []descriptors.Provider{p} - - case i == len(models.Providers)-1: - row = append(row, p) - for j := len(row); j < nbCol; j++ { - row = append(row, descriptors.Provider{}) - } - - matrix = append(matrix, row) - - default: - row = append(row, p) - } - } - - if len(row) < nbCol { - for j := len(row); j < nbCol; j++ { - row = append(row, descriptors.Provider{}) - } - - matrix = append(matrix, row) - } - - return matrix -} diff --git a/internal/dns/docs/templates/readme.md.tmpl b/internal/dns/docs/templates/readme.md.tmpl deleted file mode 100644 index 09cb10dae..000000000 --- a/internal/dns/docs/templates/readme.md.tmpl +++ /dev/null @@ -1,11 +0,0 @@ - - -{{- range . -}} - - {{- range . }} - - {{- end }} - -{{- end -}} -
{{if .Code }}{{ .Name }}{{end}}
- diff --git a/internal/dns/providers/dns_providers.go.tmpl b/internal/dns/providers/dns_providers.go.tmpl deleted file mode 100644 index c974ef6a9..000000000 --- a/internal/dns/providers/dns_providers.go.tmpl +++ /dev/null @@ -1,24 +0,0 @@ -// Code generated by 'make generate-dns'; DO NOT EDIT. - -package dns - -import ( - "fmt" - - "github.com/go-acme/lego/v4/challenge" -{{- range $provider := .Providers }} - "github.com/go-acme/lego/v4/providers/dns/{{ cleanName $provider.Code }}" -{{- end}} -) - -// NewDNSChallengeProviderByName Factory for DNS providers. -func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { - switch name { -{{- range $provider := .Providers }} - case "{{ $provider.Code }}"{{range $alias := $provider.Aliases }},"{{ $alias }}"{{end}}: - return {{ cleanName $provider.Code }}.NewDNSProvider() -{{- end}} - default: - return nil, fmt.Errorf("unrecognized DNS provider: %s", name) - } -} diff --git a/internal/dns/providers/generator.go b/internal/dns/providers/generator.go deleted file mode 100644 index df3f8a2e6..000000000 --- a/internal/dns/providers/generator.go +++ /dev/null @@ -1,75 +0,0 @@ -package main - -//go:generate go run . - -import ( - "bytes" - _ "embed" - "fmt" - "go/format" - "log" - "os" - "path/filepath" - "strings" - "text/template" - - "github.com/go-acme/lego/v4/internal/dns/descriptors" -) - -const ( - root = "../../../" - - outputPath = "providers/dns/zz_gen_dns_providers.go" -) - -//go:embed dns_providers.go.tmpl -var srcTemplate string - -func main() { - err := generate() - if err != nil { - log.Fatal(err) - } -} - -func generate() error { - info, err := descriptors.GetProviderInformation(root) - if err != nil { - return err - } - - file, err := os.Create(filepath.Join(root, outputPath)) - if err != nil { - return err - } - - defer func() { _ = file.Close() }() - - b := &bytes.Buffer{} - - err = template.Must( - template.New("").Funcs(map[string]any{ - "cleanName": func(src string) string { - return strings.ReplaceAll(src, "-", "") - }, - }).Parse(srcTemplate), - ).Execute(b, info) - if err != nil { - return err - } - - // gofmt - source, err := format.Source(b.Bytes()) - if err != nil { - return err - } - - _, err = file.Write(source) - if err != nil { - return err - } - - fmt.Printf("Switch mapping for %d DNS providers has been generated.\n", len(info.Providers)+1) - - return nil -} diff --git a/internal/clihelp/generator.go b/internal/dnsdocs/cli_help/generator.go similarity index 97% rename from internal/clihelp/generator.go rename to internal/dnsdocs/cli_help/generator.go index fcabde015..4f87ab654 100644 --- a/internal/clihelp/generator.go +++ b/internal/dnsdocs/cli_help/generator.go @@ -14,7 +14,7 @@ import ( "github.com/urfave/cli/v2" ) -const outputFile = "../../docs/data/zz_cli_help.toml" +const outputFile = "../../../docs/data/zz_cli_help.toml" const baseTemplate = `# THIS FILE IS AUTO-GENERATED. PLEASE DO NOT EDIT. @@ -50,7 +50,6 @@ func generate() error { // collect output of various help pages var help []commandHelp - for _, args := range [][]string{ {"lego", "help"}, {"lego", "help", "run"}, @@ -73,9 +72,7 @@ func generate() error { } err = outputTpl.Execute(f, help) - defer func() { _ = f.Close() }() - if err != nil { return fmt.Errorf("failed to write cli_help.toml: %w", err) } @@ -101,11 +98,9 @@ func createStubApp() *cli.App { func run(app *cli.App, args []string) (h commandHelp, err error) { w := app.Writer - defer func() { app.Writer = w }() var buf bytes.Buffer - app.Writer = &buf if err := app.Run(args); err != nil { diff --git a/internal/dns/docs/templates/dns.go.tmpl b/internal/dnsdocs/dns.go.tmpl similarity index 89% rename from internal/dns/docs/templates/dns.go.tmpl rename to internal/dnsdocs/dns.go.tmpl index c1896c91a..6ccadb456 100644 --- a/internal/dns/docs/templates/dns.go.tmpl +++ b/internal/dnsdocs/dns.go.tmpl @@ -1,7 +1,8 @@ -// Code generated by 'make generate-dns'; DO NOT EDIT. - package cmd +// CODE GENERATED AUTOMATICALLY +// THIS FILE MUST NOT BE EDITED BY HAND + import ( "fmt" "io" @@ -12,6 +13,7 @@ import ( func allDNSCodes() string { providers := []string{ + "manual", {{- range $provider := .Providers }} "{{ $provider.Code }}", {{- end}} @@ -47,6 +49,8 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/{{ $provider.Code }}`) {{end}} + case "manual": + ew.writeln(`Solving the DNS-01 challenge using CLI prompt.`) default: return fmt.Errorf("%q is not yet supported", name) } diff --git a/internal/dns/docs/templates/dns.md.tmpl b/internal/dnsdocs/dns.md.tmpl similarity index 91% rename from internal/dns/docs/templates/dns.md.tmpl rename to internal/dnsdocs/dns.md.tmpl index 95496db40..1bf7a6077 100644 --- a/internal/dns/docs/templates/dns.md.tmpl +++ b/internal/dnsdocs/dns.md.tmpl @@ -47,7 +47,7 @@ _Please contribute by adding a CLI example._ {{- end}} The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{ `{{% ref "dns#configuration-and-credentials" %}}` }}). +More information [here]({{ `{{< ref "dns#configuration-and-credentials" >}}` }}). {{- end}} {{if .Configuration.Additional }} @@ -60,7 +60,7 @@ More information [here]({{ `{{% ref "dns#configuration-and-credentials" %}}` }}) {{- end}} The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. -More information [here]({{ `{{% ref "dns#configuration-and-credentials" %}}` }}). +More information [here]({{ `{{< ref "dns#configuration-and-credentials" >}}` }}). {{- end}} {{- end}} diff --git a/internal/dnsdocs/generator.go b/internal/dnsdocs/generator.go new file mode 100644 index 000000000..c19c370e8 --- /dev/null +++ b/internal/dnsdocs/generator.go @@ -0,0 +1,285 @@ +package main + +//go:generate go run . + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "go/format" + "io" + "log" + "os" + "path/filepath" + "sort" + "strings" + "text/template" + + "github.com/BurntSushi/toml" +) + +const ( + root = "../../" + dnsPackage = root + "providers/dns" + mdTemplate = root + "internal/dnsdocs/dns.md.tmpl" + cliTemplate = root + "internal/dnsdocs/dns.go.tmpl" + cliOutput = root + "cmd/zz_gen_cmd_dnshelp.go" + docOutput = root + "docs/content/dns" + readmePath = root + "README.md" +) + +const ( + startLine = "" + endLine = "" +) + +type Model struct { + Name string // Real name of the DNS provider + Code string // DNS code + Since string // First lego version + URL string // DNS provider URL + Description string // Provider summary + Example string // CLI example + Configuration *Configuration // Environment variables + Links *Links // Links + Additional string // Extra documentation + GeneratedFrom string // Source file +} + +type Configuration struct { + Credentials map[string]string + Additional map[string]string +} + +type Links struct { + API string + GoClient string +} + +type Providers struct { + Providers []Model +} + +func main() { + models := &Providers{} + + err := filepath.Walk(dnsPackage, walker(models)) + if err != nil { + log.Fatal(err) + } + + // generate CLI help + err = generateCLIHelp(models) + if err != nil { + log.Fatal(err) + } + + // generate README.md + err = generateReadMe(models) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Documentation for %d DNS providers has been generated.\n", len(models.Providers)+1) +} + +func walker(prs *Providers) func(string, os.FileInfo, error) error { + return func(path string, _ os.FileInfo, err error) error { + if err != nil { + return err + } + + if filepath.Ext(path) == ".toml" { + m := Model{} + + m.GeneratedFrom, err = filepath.Rel(root, path) + if err != nil { + return err + } + + _, err := toml.DecodeFile(path, &m) + if err != nil { + return err + } + + prs.Providers = append(prs.Providers, m) + + // generate documentation + return generateDocumentation(m) + } + + return nil + } +} + +func generateDocumentation(m Model) error { + filename := filepath.Join(docOutput, "zz_gen_"+m.Code+".md") + + file, err := os.Create(filename) + if err != nil { + return err + } + + return template.Must(template.ParseFiles(mdTemplate)).Execute(file, m) +} + +func generateCLIHelp(models *Providers) error { + filename := filepath.Clean(cliOutput) + + file, err := os.Create(filename) + if err != nil { + return err + } + + tlt := template.New(filepath.Base(cliTemplate)).Funcs(map[string]interface{}{ + "safe": func(src string) string { + return strings.ReplaceAll(src, "`", "'") + }, + }) + + b := &bytes.Buffer{} + err = template.Must(tlt.ParseFiles(cliTemplate)).Execute(b, models) + if err != nil { + return err + } + + // gofmt + source, err := format.Source(b.Bytes()) + if err != nil { + return err + } + + _, err = file.Write(source) + return err +} + +func generateReadMe(models *Providers) error { + maximum, lines := extractTableData(models) + + file, err := os.Open(readmePath) + if err != nil { + return err + } + + defer func() { _ = file.Close() }() + + var skip bool + + buffer := bytes.NewBufferString("") + + fileScanner := bufio.NewScanner(file) + for fileScanner.Scan() { + text := fileScanner.Text() + + if text == startLine { + _, _ = fmt.Fprintln(buffer, text) + err = writeDNSTable(buffer, lines, maximum) + if err != nil { + return err + } + skip = true + } + + if text == endLine { + skip = false + } + + if skip { + continue + } + + _, _ = fmt.Fprintln(buffer, text) + } + + if fileScanner.Err() != nil { + return fileScanner.Err() + } + + if skip { + return errors.New("missing end tag") + } + + return os.WriteFile(readmePath, buffer.Bytes(), 0o666) +} + +func extractTableData(models *Providers) (int, [][]string) { + readmePattern := "[%s](https://go-acme.github.io/lego/dns/%s/)" + + items := []string{fmt.Sprintf(readmePattern, "Manual", "manual")} + + var maximum int + + for _, pvd := range models.Providers { + item := fmt.Sprintf(readmePattern, strings.ReplaceAll(pvd.Name, "|", "/"), pvd.Code) + items = append(items, item) + + if maximum < len(item) { + maximum = len(item) + } + } + + const nbCol = 4 + + sort.Slice(items, func(i, j int) bool { + return strings.ToLower(items[i]) < strings.ToLower(items[j]) + }) + + var lines [][]string + var line []string + + for i, item := range items { + switch { + case len(line) == nbCol: + lines = append(lines, line) + line = []string{item} + + case i == len(items)-1: + line = append(line, item) + for j := len(line); j < nbCol; j++ { + line = append(line, "") + } + lines = append(lines, line) + + default: + line = append(line, item) + } + } + + if len(line) < nbCol { + for j := len(line); j < nbCol; j++ { + line = append(line, "") + } + lines = append(lines, line) + } + + return maximum, lines +} + +func writeDNSTable(w io.Writer, lines [][]string, size int) error { + _, err := fmt.Fprintf(w, "\n") + if err != nil { + return err + } + + _, err = fmt.Fprintf(w, "|%[1]s|%[1]s|%[1]s|%[1]s|\n", strings.Repeat(" ", size+2)) + if err != nil { + return err + } + + _, err = fmt.Fprintf(w, "|%[1]s|%[1]s|%[1]s|%[1]s|\n", strings.Repeat("-", size+2)) + if err != nil { + return err + } + + linePattern := fmt.Sprintf("| %%-%[1]ds | %%-%[1]ds | %%-%[1]ds | %%-%[1]ds |\n", size) + for _, line := range lines { + _, err = fmt.Fprintf(w, linePattern, line[0], line[1], line[2], line[3]) + if err != nil { + return err + } + } + + _, err = fmt.Fprintf(w, "\n") + return err +} diff --git a/internal/release.go b/internal/release.go new file mode 100644 index 000000000..b03e62b2b --- /dev/null +++ b/internal/release.go @@ -0,0 +1,223 @@ +package main + +import ( + "bytes" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "log" + "os" + "regexp" + "strconv" + "strings" + "text/template" + + "github.com/urfave/cli/v2" +) + +const sourceFile = "./acme/api/internal/sender/useragent.go" + +const uaTemplate = `package sender + +// CODE GENERATED AUTOMATICALLY +// THIS FILE MUST NOT BE EDITED BY HAND + +const ( + // ourUserAgent is the User-Agent of this underlying library package. + ourUserAgent = "xenolf-acme/{{ .version }}" + + // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. + // values: detach|release + // NOTE: Update this with each tagged release. + ourUserAgentComment = "{{ .comment }}" +) + +` + +func main() { + app := cli.NewApp() + app.Name = "lego-releaser" + app.Usage = "Lego releaser" + app.HelpName = "releaser" + app.Commands = []*cli.Command{ + { + Name: "release", + Usage: "Update file for a release", + Action: release, + Before: func(ctx *cli.Context) error { + mode := ctx.String("mode") + switch mode { + case "patch", "minor", "major": + return nil + default: + return fmt.Errorf("invalid mode: %s", mode) + } + }, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "mode", + Aliases: []string{"m"}, + Value: "patch", + Usage: "The release mode: patch|minor|major", + }, + }, + }, + { + Name: "detach", + Usage: "Update file post release", + Action: detach, + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} + +func release(ctx *cli.Context) error { + mode := ctx.String("mode") + + // Read file + data, err := readUserAgentFile(sourceFile) + if err != nil { + return err + } + + // Bump version + newVersion, err := bumpVersion(data["ourUserAgent"], mode) + if err != nil { + return err + } + + // Write file + comment := "release" // detach|release + return writeUserAgentFile(sourceFile, newVersion, comment) +} + +func detach(_ *cli.Context) error { + // Read file + data, err := readUserAgentFile(sourceFile) + if err != nil { + return err + } + + // Write file + version := strings.TrimPrefix(data["ourUserAgent"], "xenolf-acme/") + comment := "detach" + return writeUserAgentFile(sourceFile, version, comment) +} + +type visitor struct { + data map[string]string +} + +func (v visitor) Visit(n ast.Node) ast.Visitor { + if n == nil { + return nil + } + + switch d := n.(type) { + case *ast.GenDecl: + if d.Tok == token.CONST { + for _, spec := range d.Specs { + valueSpec, ok := spec.(*ast.ValueSpec) + if !ok { + continue + } + if len(valueSpec.Names) != 1 || len(valueSpec.Values) != 1 { + continue + } + + va, ok := valueSpec.Values[0].(*ast.BasicLit) + if !ok { + continue + } + if va.Kind != token.STRING { + continue + } + + s, err := strconv.Unquote(va.Value) + if err != nil { + continue + } + + v.data[valueSpec.Names[0].String()] = s + } + } + default: + // noop + } + return v +} + +func readUserAgentFile(filename string) (map[string]string, error) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, filename, nil, parser.AllErrors) + if err != nil { + return nil, err + } + + v := visitor{data: make(map[string]string)} + ast.Walk(v, file) + + return v.data, nil +} + +func writeUserAgentFile(filename, version, comment string) error { + tmpl, err := template.New("ua").Parse(uaTemplate) + if err != nil { + return err + } + + b := &bytes.Buffer{} + err = tmpl.Execute(b, map[string]string{ + "version": version, + "comment": comment, + }) + if err != nil { + return err + } + + source, err := format.Source(b.Bytes()) + if err != nil { + return err + } + + return os.WriteFile(filename, source, 0o644) +} + +func bumpVersion(userAgent, mode string) (string, error) { + prevVersion := strings.TrimPrefix(userAgent, "xenolf-acme/") + + allString := regexp.MustCompile(`(\d+)\.(\d+)\.(\d+)`).FindStringSubmatch(prevVersion) + + if len(allString) != 4 { + return "", fmt.Errorf("invalid version format: %s", prevVersion) + } + + switch mode { + case "patch": + patch, err := strconv.Atoi(allString[3]) + if err != nil { + return "", err + } + return fmt.Sprintf("%s.%s.%d", allString[1], allString[2], patch+1), nil + case "minor": + minor, err := strconv.Atoi(allString[2]) + if err != nil { + return "", err + } + return fmt.Sprintf("%s.%d.0", allString[1], minor+1), nil + case "major": + major, err := strconv.Atoi(allString[1]) + if err != nil { + return "", err + } + return fmt.Sprintf("%d.0.0", major+1), nil + default: + return "", fmt.Errorf("invalid mode: %s", mode) + } +} diff --git a/internal/releaser/generator.go b/internal/releaser/generator.go deleted file mode 100644 index f24aea25f..000000000 --- a/internal/releaser/generator.go +++ /dev/null @@ -1,84 +0,0 @@ -package main - -import ( - "bytes" - "embed" - "fmt" - "go/format" - "os" - "path/filepath" - "text/template" -) - -const ( - dnsTemplate = "templates/dns.go.tmpl" - dnsTargetFile = "./providers/dns/internal/useragent/useragent.go" -) - -const ( - senderTemplate = "templates/sender.go.tmpl" - senderTargetFile = "./acme/api/internal/sender/useragent.go" -) - -const ( - versionTemplate = "templates/version.go.tmpl" - versionTargetFile = "./cmd/lego/zz_gen_version.go" -) - -//go:embed templates -var templateFS embed.FS - -type Generator struct { - templatePath string - targetFile string -} - -func NewGenerator(templatePath, targetFile string) *Generator { - return &Generator{templatePath: templatePath, targetFile: targetFile} -} - -func (g *Generator) Generate(version, comment string) error { - tmpl, err := template.New(filepath.Base(g.templatePath)).ParseFS(templateFS, g.templatePath) - if err != nil { - return fmt.Errorf("parsing template (%s): %w", g.templatePath, err) - } - - b := &bytes.Buffer{} - - err = tmpl.Execute(b, map[string]string{ - "version": version, - "comment": comment, - }) - if err != nil { - return fmt.Errorf("execute template (%s): %w", g.templatePath, err) - } - - source, err := format.Source(b.Bytes()) - if err != nil { - return fmt.Errorf("format generated content (%s): %w", g.targetFile, err) - } - - err = os.WriteFile(g.targetFile, source, 0o644) - if err != nil { - return fmt.Errorf("write file (%s): %w", g.targetFile, err) - } - - return nil -} - -func generate(targetVersion, comment string) error { - generators := []*Generator{ - NewGenerator(dnsTemplate, dnsTargetFile), - NewGenerator(senderTemplate, senderTargetFile), - NewGenerator(versionTemplate, versionTargetFile), - } - - for _, generator := range generators { - err := generator.Generate(targetVersion, comment) - if err != nil { - return fmt.Errorf("generate file(s): %w", err) - } - } - - return nil -} diff --git a/internal/releaser/releaser.go b/internal/releaser/releaser.go deleted file mode 100644 index 57b463933..000000000 --- a/internal/releaser/releaser.go +++ /dev/null @@ -1,187 +0,0 @@ -package main - -import ( - "fmt" - "go/ast" - "go/parser" - "go/token" - "log" - "os" - "strconv" - - hcversion "github.com/hashicorp/go-version" - "github.com/urfave/cli/v2" -) - -const flgMode = "mode" - -const ( - modePatch = "patch" - modeMinor = "minor" - modeMajor = "major" -) - -const versionSourceFile = "./cmd/lego/zz_gen_version.go" - -const ( - commentRelease = "release" - commentDetach = "detach" -) - -func main() { - app := cli.NewApp() - app.Name = "lego-releaser" - app.Usage = "Lego releaser" - app.HelpName = "releaser" - app.Commands = []*cli.Command{ - { - Name: "release", - Usage: "Update file for a release", - Action: release, - Before: func(ctx *cli.Context) error { - mode := ctx.String("mode") - switch mode { - case modePatch, modeMinor, modeMajor: - return nil - default: - return fmt.Errorf("invalid mode: %s", mode) - } - }, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: flgMode, - Aliases: []string{"m"}, - Value: modePatch, - Usage: fmt.Sprintf("The release mode: %s|%s|%s", modePatch, modeMinor, modeMajor), - }, - }, - }, - { - Name: "detach", - Usage: "Update file post release", - Action: detach, - }, - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} - -func release(ctx *cli.Context) error { - mode := ctx.String(flgMode) - - currentVersion, err := readCurrentVersion(versionSourceFile) - if err != nil { - return fmt.Errorf("read current version: %w", err) - } - - nextVersion, err := bumpVersion(mode, currentVersion) - if err != nil { - return fmt.Errorf("bump version: %w", err) - } - - err = generate(nextVersion, commentRelease) - if err != nil { - return err - } - - return nil -} - -func detach(_ *cli.Context) error { - currentVersion, err := readCurrentVersion(versionSourceFile) - if err != nil { - return fmt.Errorf("read current version: %w", err) - } - - v := currentVersion.Core().String() - - err = generate(v, commentDetach) - if err != nil { - return err - } - - return nil -} - -func readCurrentVersion(filename string) (*hcversion.Version, error) { - fset := token.NewFileSet() - - file, err := parser.ParseFile(fset, filename, nil, parser.AllErrors) - if err != nil { - return nil, err - } - - v := visitor{data: make(map[string]string)} - ast.Walk(v, file) - - current, err := hcversion.NewSemver(v.data["defaultVersion"]) - if err != nil { - return nil, err - } - - return current, nil -} - -type visitor struct { - data map[string]string -} - -func (v visitor) Visit(n ast.Node) ast.Visitor { - if n == nil { - return nil - } - - switch d := n.(type) { - case *ast.GenDecl: - if d.Tok == token.CONST { - for _, spec := range d.Specs { - valueSpec, ok := spec.(*ast.ValueSpec) - if !ok { - continue - } - - if len(valueSpec.Names) != 1 || len(valueSpec.Values) != 1 { - continue - } - - va, ok := valueSpec.Values[0].(*ast.BasicLit) - if !ok { - continue - } - - if va.Kind != token.STRING { - continue - } - - s, err := strconv.Unquote(va.Value) - if err != nil { - continue - } - - v.data[valueSpec.Names[0].String()] = s - } - } - default: - // noop - } - - return v -} - -func bumpVersion(mode string, v *hcversion.Version) (string, error) { - segments := v.Segments() - - switch mode { - case modePatch: - return fmt.Sprintf("%d.%d.%d", segments[0], segments[1], segments[2]+1), nil - case modeMinor: - return fmt.Sprintf("%d.%d.0", segments[0], segments[1]+1), nil - case modeMajor: - return fmt.Sprintf("%d.0.0", segments[0]+1), nil - default: - return "", fmt.Errorf("invalid mode: %s", mode) - } -} diff --git a/internal/releaser/templates/dns.go.tmpl b/internal/releaser/templates/dns.go.tmpl deleted file mode 100644 index 0e5cd65d7..000000000 --- a/internal/releaser/templates/dns.go.tmpl +++ /dev/null @@ -1,29 +0,0 @@ -// Code generated by 'internal/releaser'; DO NOT EDIT. - -package useragent - -import ( - "fmt" - "net/http" - "runtime" -) - -const ( - // ourUserAgent is the User-Agent of this underlying library package. - ourUserAgent = "goacme-lego/{{ .version }}" - - // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. - // values: detach|release - // NOTE: Update this with each tagged release. - ourUserAgentComment = "{{ .comment }}" -) - -// Get builds and returns the User-Agent string. -func Get() string { - return fmt.Sprintf("%s (%s; %s; %s)", ourUserAgent, ourUserAgentComment, runtime.GOOS, runtime.GOARCH) -} - -// SetHeader sets the User-Agent header. -func SetHeader(h http.Header) { - h.Set("User-Agent", Get()) -} diff --git a/internal/releaser/templates/sender.go.tmpl b/internal/releaser/templates/sender.go.tmpl deleted file mode 100644 index c07253841..000000000 --- a/internal/releaser/templates/sender.go.tmpl +++ /dev/null @@ -1,13 +0,0 @@ -// Code generated by 'internal/releaser'; DO NOT EDIT. - -package sender - -const ( - // ourUserAgent is the User-Agent of this underlying library package. - ourUserAgent = "xenolf-acme/{{ .version }}" - - // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. - // values: detach|release - // NOTE: Update this with each tagged release. - ourUserAgentComment = "{{ .comment }}" -) diff --git a/internal/releaser/templates/version.go.tmpl b/internal/releaser/templates/version.go.tmpl deleted file mode 100644 index 0c2512047..000000000 --- a/internal/releaser/templates/version.go.tmpl +++ /dev/null @@ -1,15 +0,0 @@ -// Code generated by 'internal/releaser'; DO NOT EDIT. - -package main - -const defaultVersion = "v{{ .version }}+dev{{ if .comment }}-{{ .comment }}{{end}}" - -var version = "" - -func getVersion() string { - if version == "" { - return defaultVersion - } - - return version -} diff --git a/lego/client.go b/lego/client.go index d06956203..ef72a2889 100644 --- a/lego/client.go +++ b/lego/client.go @@ -53,15 +53,7 @@ func NewClient(config *Config) (*Client, error) { solversManager := resolver.NewSolversManager(core) prober := resolver.NewProber(solversManager) - - options := certificate.CertifierOptions{ - KeyType: config.Certificate.KeyType, - Timeout: config.Certificate.Timeout, - OverallRequestLimit: config.Certificate.OverallRequestLimit, - DisableCommonName: config.Certificate.DisableCommonName, - } - - certifier := certificate.NewCertifier(core, prober, options) + certifier := certificate.NewCertifier(core, prober, certificate.CertifierOptions{KeyType: config.Certificate.KeyType, Timeout: config.Certificate.Timeout}) return &Client{ Certificate: certifier, diff --git a/lego/client_config.go b/lego/client_config.go index 969135a13..27bc1872d 100644 --- a/lego/client_config.go +++ b/lego/client_config.go @@ -61,10 +61,8 @@ func NewConfig(user registration.User) *Config { } type CertificateConfig struct { - KeyType certcrypto.KeyType - Timeout time.Duration - OverallRequestLimit int - DisableCommonName bool + KeyType certcrypto.KeyType + Timeout time.Duration } // createDefaultHTTPClient Creates an HTTP client with a reasonable timeout value @@ -101,41 +99,26 @@ func initCertPool() *x509.CertPool { return nil } - useSystemCertPool, _ := strconv.ParseBool(os.Getenv(caSystemCertPool)) + certPool := getCertPool() - caCerts := strings.Split(customCACertsPath, string(os.PathListSeparator)) + for _, customPath := range strings.Split(customCACertsPath, string(os.PathListSeparator)) { + customCAs, err := os.ReadFile(customPath) + if err != nil { + panic(fmt.Sprintf("error reading %s=%q: %v", + caCertificatesEnvVar, customPath, err)) + } - certPool, err := CreateCertPool(caCerts, useSystemCertPool) - if err != nil { - panic(fmt.Sprintf("create certificates pool: %v", err)) + if ok := certPool.AppendCertsFromPEM(customCAs); !ok { + panic(fmt.Sprintf("error creating x509 cert pool from %s=%q: %v", + caCertificatesEnvVar, customPath, err)) + } } return certPool } -// CreateCertPool creates a *x509.CertPool populated with the PEM certificates. -func CreateCertPool(caCerts []string, useSystemCertPool bool) (*x509.CertPool, error) { - if len(caCerts) == 0 { - return nil, nil - } - - certPool := newCertPool(useSystemCertPool) - - for _, customPath := range caCerts { - customCAs, err := os.ReadFile(customPath) - if err != nil { - return nil, fmt.Errorf("error reading %q: %w", customPath, err) - } - - if ok := certPool.AppendCertsFromPEM(customCAs); !ok { - return nil, fmt.Errorf("error creating x509 cert pool from %q: %w", customPath, err) - } - } - - return certPool, nil -} - -func newCertPool(useSystemCertPool bool) *x509.CertPool { +func getCertPool() *x509.CertPool { + useSystemCertPool, _ := strconv.ParseBool(os.Getenv(caSystemCertPool)) if !useSystemCertPool { return x509.NewCertPool() } @@ -144,6 +127,5 @@ func newCertPool(useSystemCertPool bool) *x509.CertPool { if err == nil { return pool } - return x509.NewCertPool() } diff --git a/lego/client_test.go b/lego/client_test.go index 63d3b0ad1..7d2f514dc 100644 --- a/lego/client_test.go +++ b/lego/client_test.go @@ -13,9 +13,10 @@ import ( ) func TestNewClient(t *testing.T) { - server := tester.MockACMEServer().BuildHTTPS(t) + _, apiURL := tester.SetupFakeAPI(t) - key, err := rsa.GenerateKey(rand.Reader, 1024) + keyBits := 32 // small value keeps test fast + key, err := rsa.GenerateKey(rand.Reader, keyBits) require.NoError(t, err, "Could not generate test key") user := mockUser{ @@ -25,8 +26,7 @@ func TestNewClient(t *testing.T) { } config := NewConfig(user) - config.CADirURL = server.URL + "/dir" - config.HTTPClient = server.Client() + config.CADirURL = apiURL + "/dir" client, err := NewClient(config) require.NoError(t, err, "Could not create client") diff --git a/log/logger.go b/log/logger.go index 2f700a359..48a81fad0 100644 --- a/log/logger.go +++ b/log/logger.go @@ -10,50 +10,50 @@ var Logger StdLogger = log.New(os.Stderr, "", log.LstdFlags) // StdLogger interface for Standard Logger. type StdLogger interface { - Fatal(args ...any) - Fatalln(args ...any) - Fatalf(format string, args ...any) - Print(args ...any) - Println(args ...any) - Printf(format string, args ...any) + Fatal(args ...interface{}) + Fatalln(args ...interface{}) + Fatalf(format string, args ...interface{}) + Print(args ...interface{}) + Println(args ...interface{}) + Printf(format string, args ...interface{}) } // Fatal writes a log entry. // It uses Logger if not nil, otherwise it uses the default log.Logger. -func Fatal(args ...any) { +func Fatal(args ...interface{}) { Logger.Fatal(args...) } // Fatalf writes a log entry. // It uses Logger if not nil, otherwise it uses the default log.Logger. -func Fatalf(format string, args ...any) { +func Fatalf(format string, args ...interface{}) { Logger.Fatalf(format, args...) } // Print writes a log entry. // It uses Logger if not nil, otherwise it uses the default log.Logger. -func Print(args ...any) { +func Print(args ...interface{}) { Logger.Print(args...) } // Println writes a log entry. // It uses Logger if not nil, otherwise it uses the default log.Logger. -func Println(args ...any) { +func Println(args ...interface{}) { Logger.Println(args...) } // Printf writes a log entry. // It uses Logger if not nil, otherwise it uses the default log.Logger. -func Printf(format string, args ...any) { +func Printf(format string, args ...interface{}) { Logger.Printf(format, args...) } // Warnf writes a log entry. -func Warnf(format string, args ...any) { +func Warnf(format string, args ...interface{}) { Printf("[WARN] "+format, args...) } // Infof writes a log entry. -func Infof(format string, args ...any) { +func Infof(format string, args ...interface{}) { Printf("[INFO] "+format, args...) } diff --git a/platform/config/env/env.go b/platform/config/env/env.go index 33a0d6caa..3fd1e3a1a 100644 --- a/platform/config/env/env.go +++ b/platform/config/env/env.go @@ -16,13 +16,11 @@ func Get(names ...string) (map[string]string, error) { values := map[string]string{} var missingEnvVars []string - for _, envVar := range names { value := GetOrFile(envVar) if value == "" { missingEnvVars = append(missingEnvVars, envVar) } - values[envVar] = value } @@ -60,7 +58,6 @@ func GetWithFallback(groups ...[]string) (map[string]string, error) { values := map[string]string{} var missingEnvVars []string - for _, names := range groups { if len(names) == 0 { return nil, errors.New("undefined environment variable names") @@ -71,7 +68,6 @@ func GetWithFallback(groups ...[]string) (map[string]string, error) { missingEnvVars = append(missingEnvVars, envVar) continue } - values[envVar] = value } @@ -111,7 +107,7 @@ func getOneWithFallback(main string, names ...string) (string, string) { // GetOrDefaultString returns the given environment variable value as a string. // Returns the default if the env var cannot be found. -func GetOrDefaultString(envVar, defaultValue string) string { +func GetOrDefaultString(envVar string, defaultValue string) string { return getOrDefault(envVar, defaultValue, ParseString) } @@ -152,7 +148,6 @@ func GetOrFile(envVar string) string { } fileVar := envVar + "_FILE" - fileVarValue := os.Getenv(fileVar) if fileVarValue == "" { return envVarValue @@ -189,20 +184,3 @@ func ParseString(s string) (string, error) { return s, nil } - -// ParsePairs parses a raw string of comma-separated key-value pairs into a map. -// Keys and values are separated by a colon and are trimmed of whitespace. -func ParsePairs(raw string) (map[string]string, error) { - result := make(map[string]string) - - for pair := range strings.SplitSeq(strings.TrimSuffix(raw, ","), ",") { - data := strings.Split(pair, ":") - if len(data) != 2 { - return nil, fmt.Errorf("incorrect pair: %s", pair) - } - - result[strings.TrimSpace(data[0])] = strings.TrimSpace(data[1]) - } - - return result, nil -} diff --git a/platform/config/env/env_test.go b/platform/config/env/env_test.go index b131d4d91..4a3d0a04c 100644 --- a/platform/config/env/env_test.go +++ b/platform/config/env/env_test.go @@ -367,10 +367,9 @@ func TestGetOrFile_ReadsFiles(t *testing.T) { err = os.Unsetenv(varEnvName) require.NoError(t, err) - file, err := os.CreateTemp(t.TempDir(), "lego") + file, err := os.CreateTemp("", "lego") require.NoError(t, err) - - t.Cleanup(func() { _ = file.Close() }) + defer os.Remove(file.Name()) err = os.WriteFile(file.Name(), []byte("lego_file\n"), 0o644) require.NoError(t, err) @@ -393,10 +392,9 @@ func TestGetOrFile_PrefersEnvVars(t *testing.T) { err = os.Unsetenv(varEnvName) require.NoError(t, err) - file, err := os.CreateTemp(t.TempDir(), "lego") + file, err := os.CreateTemp("", "lego") require.NoError(t, err) - - t.Cleanup(func() { _ = file.Close() }) + defer os.Remove(file.Name()) err = os.WriteFile(file.Name(), []byte("lego_file"), 0o644) require.NoError(t, err) @@ -408,77 +406,3 @@ func TestGetOrFile_PrefersEnvVars(t *testing.T) { assert.Equal(t, "lego_env", value) } - -func TestParsePairs(t *testing.T) { - testCases := []struct { - desc string - value string - expected map[string]string - }{ - { - desc: "one pair", - value: "foo:bar", - expected: map[string]string{"foo": "bar"}, - }, - { - desc: "multiple pairs", - value: "foo:bar,a:b,c:d", - expected: map[string]string{"a": "b", "c": "d", "foo": "bar"}, - }, - { - desc: "multiple pairs with spaces", - value: "foo:bar, a:b , c: d", - expected: map[string]string{"a": "b", "c": "d", "foo": "bar"}, - }, - { - desc: "empty value pair", - value: "foo:", - expected: map[string]string{"foo": ""}, - }, - { - desc: "empty key pair", - value: ":bar", - expected: map[string]string{"": "bar"}, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - pairs, err := ParsePairs(test.value) - require.NoError(t, err) - - assert.Equal(t, test.expected, pairs) - }) - } -} - -func TestParsePairs_error(t *testing.T) { - testCases := []struct { - desc string - value string - }{ - { - desc: "empty value", - value: "", - }, - { - desc: "multiple colons", - value: "foo:bar:bir", - }, - { - desc: "valid pair and multiple colons", - value: "a:b,foo:bar:bir", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - _, err := ParsePairs(test.value) - require.Error(t, err) - }) - } -} diff --git a/platform/tester/api.go b/platform/tester/api.go index 8343b487f..175530f96 100644 --- a/platform/tester/api.go +++ b/platform/tester/api.go @@ -2,47 +2,63 @@ package tester import ( "encoding/json" - "fmt" "net/http" "net/http/httptest" + "testing" "github.com/go-acme/lego/v4/acme" - "github.com/go-acme/lego/v4/platform/tester/servermock" ) -// MockACMEServer Minimal stub ACME server for validation. -func MockACMEServer() *servermock.Builder[*httptest.Server] { - return servermock.NewBuilder( - func(server *httptest.Server) (*httptest.Server, error) { - return server, nil - }). - Route("GET /dir", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - serverURL := fmt.Sprintf("https://%s", req.Context().Value(http.LocalAddrContextKey)) +// SetupFakeAPI Minimal stub ACME server for validation. +func SetupFakeAPI(t *testing.T) (*http.ServeMux, string) { + t.Helper() - servermock.JSONEncode(acme.Directory{ - NewNonceURL: serverURL + "/nonce", - NewAccountURL: serverURL + "/account", - NewOrderURL: serverURL + "/newOrder", - RevokeCertURL: serverURL + "/revokeCert", - KeyChangeURL: serverURL + "/keyChange", - RenewalInfo: serverURL + "/renewalInfo", - }).ServeHTTP(rw, req) - })). - Route("HEAD /nonce", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.Header().Set("Replay-Nonce", "12345") - rw.Header().Set("Retry-After", "0") - })) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc("/dir", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + err := WriteJSONResponse(w, acme.Directory{ + NewNonceURL: server.URL + "/nonce", + NewAccountURL: server.URL + "/account", + NewOrderURL: server.URL + "/newOrder", + RevokeCertURL: server.URL + "/revokeCert", + KeyChangeURL: server.URL + "/keyChange", + RenewalInfo: server.URL + "/renewalInfo", + }) + + mux.HandleFunc("/nonce", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodHead { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + w.Header().Set("Replay-Nonce", "12345") + w.Header().Set("Retry-After", "0") + }) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + return mux, server.URL } // WriteJSONResponse marshals the body as JSON and writes it to the response. -func WriteJSONResponse(w http.ResponseWriter, body any) error { +func WriteJSONResponse(w http.ResponseWriter, body interface{}) error { bs, err := json.Marshal(body) if err != nil { return err } w.Header().Set("Content-Type", "application/json") - if _, err := w.Write(bs); err != nil { return err } diff --git a/platform/tester/dnsmock/dnsmock.go b/platform/tester/dnsmock/dnsmock.go deleted file mode 100644 index 6cb4f45b8..000000000 --- a/platform/tester/dnsmock/dnsmock.go +++ /dev/null @@ -1,191 +0,0 @@ -package dnsmock - -import ( - "fmt" - "math" - "net" - "strings" - "sync" - "testing" - "time" - - "github.com/miekg/dns" - "github.com/stretchr/testify/require" -) - -const noType uint16 = math.MaxUint16 - -type Option func(*dns.Server) error - -type Builder struct { - // domain -> op -> type - routes map[string]map[int]map[uint16]dns.Handler - - stringToType map[string]uint16 -} - -func NewServer() *Builder { - stringToType := make(map[string]uint16) - for typ, str := range dns.TypeToString { - stringToType[str] = typ - } - - return &Builder{ - routes: make(map[string]map[int]map[uint16]dns.Handler), - stringToType: stringToType, - } -} - -func (b *Builder) Query(pattern string, handler dns.HandlerFunc) *Builder { - route, err := b.route(pattern, dns.OpcodeQuery, handler) - if err != nil { - panic(err.Error()) - } - - return route -} - -func (b *Builder) Update(pattern string, handler dns.HandlerFunc) *Builder { - route, err := b.route(pattern, dns.OpcodeUpdate, handler) - if err != nil { - panic(err.Error()) - } - - return route -} - -func (b *Builder) route(pattern string, op int, handler dns.HandlerFunc) (*Builder, error) { - parts := strings.Fields(pattern) - - domain := parts[0] - - _, ok := dns.IsDomainName(domain) - if !ok { - return nil, fmt.Errorf("%s: invalid domain: %s", dns.OpcodeToString[op], domain) - } - - if _, ok := b.routes[domain]; !ok { - b.routes[domain] = make(map[int]map[uint16]dns.Handler) - } - - if _, ok := b.routes[domain][op]; !ok { - b.routes[domain][op] = make(map[uint16]dns.Handler) - } - - if _, ok := b.routes[domain][op][noType]; ok { - return nil, fmt.Errorf("%s: a global route already exists for the domain: %s", dns.OpcodeToString[op], domain) - } - - switch len(parts) { - case 1: - if len(b.routes[domain][op]) > 0 { - return nil, fmt.Errorf("%s: global route and specific routes cannot be mixed for the same domain: %s", dns.OpcodeToString[op], domain) - } - - b.routes[domain][op][noType] = handler - - return b, nil - - case 2: - raw := parts[1] - - qType, ok := b.stringToType[raw] - if !ok { - return nil, fmt.Errorf("%s: unknown type: %s", dns.OpcodeToString[op], raw) - } - - if _, ok := b.routes[domain][op][qType]; ok { - return nil, fmt.Errorf("%s: duplicate route: %s", dns.OpcodeToString[op], pattern) - } - - b.routes[domain][op][qType] = handler - - return b, nil - - default: - return nil, fmt.Errorf("%s: invalid pattern: %s", dns.OpcodeToString[op], pattern) - } -} - -func (b *Builder) Build(t *testing.T, options ...Option) net.Addr { - t.Helper() - - mux := dns.NewServeMux() - - server := &dns.Server{ - Addr: "127.0.0.1:0", - Net: "udp", - ReadTimeout: time.Hour, - WriteTimeout: time.Hour, - Handler: mux, - MsgAcceptFunc: func(dh dns.Header) dns.MsgAcceptAction { - // bypass defaultMsgAcceptFunc to allow dynamic update (https://github.com/miekg/dns/pull/830) - return dns.MsgAccept - }, - } - - for _, option := range options { - require.NoError(t, option(server)) - } - - for pattern, ops := range b.routes { - mux.HandleFunc(pattern, func(w dns.ResponseWriter, req *dns.Msg) { - mTypes, ok := ops[req.Opcode] - if !ok { - _ = w.WriteMsg(new(dns.Msg).SetRcode(req, dns.RcodeNotImplemented)) - - return - } - - if h, found := mTypes[noType]; found { - h.ServeDNS(w, req) - - return - } - - // For safety but it doesn't happen. - if len(req.Question) == 0 { - _ = w.WriteMsg(new(dns.Msg).SetRcode(req, dns.RcodeRefused)) - - return - } - - // For safety but it doesn't happen. - if req.Question[0].Qclass != dns.ClassINET { - _ = w.WriteMsg(new(dns.Msg).SetRcode(req, dns.RcodeRefused)) - - return - } - - // Works only for [Query]. - h, ok := mTypes[req.Question[0].Qtype] - if !ok { - _ = w.WriteMsg(new(dns.Msg).SetRcode(req, dns.RcodeNotImplemented)) - - return - } - - h.ServeDNS(w, req) - }) - } - - t.Cleanup(func() { - _ = server.Shutdown() - }) - - waitLock := sync.Mutex{} - waitLock.Lock() - - server.NotifyStartedFunc = waitLock.Unlock - - go func() { - err := server.ListenAndServe() - if err != nil { - t.Log(err) - } - }() - - waitLock.Lock() - - return server.PacketConn.LocalAddr() -} diff --git a/platform/tester/dnsmock/dnsmock_test.go b/platform/tester/dnsmock/dnsmock_test.go deleted file mode 100644 index 77a67a402..000000000 --- a/platform/tester/dnsmock/dnsmock_test.go +++ /dev/null @@ -1,240 +0,0 @@ -package dnsmock - -import ( - "testing" - "time" - - "github.com/miekg/dns" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestServer_Query_matchType(t *testing.T) { - addr := NewServer(). - Query("example.com. SOA", Noop). - Build(t) - - client := &dns.Client{Timeout: 1 * time.Second} - - m := new(dns.Msg).SetQuestion("example.com.", dns.TypeSOA) - - r, _, err := client.Exchange(m, addr.String()) - require.NoError(t, err) - - require.Equalf(t, dns.RcodeSuccess, r.Rcode, - "expected %s, got %s", dns.RcodeToString[dns.RcodeSuccess], dns.RcodeToString[r.Rcode]) - assert.Equal(t, m.Question, r.Question) -} - -func TestServer_Query_noType(t *testing.T) { - addr := NewServer(). - Query("example.com.", Noop). - Build(t) - - client := &dns.Client{Timeout: 1 * time.Second} - - m := new(dns.Msg).SetQuestion("example.com.", dns.TypeSOA) - - r, _, err := client.Exchange(m, addr.String()) - require.NoError(t, err) - - require.Equalf(t, dns.RcodeSuccess, r.Rcode, - "expected %s, got %s", dns.RcodeToString[dns.RcodeSuccess], dns.RcodeToString[r.Rcode]) - assert.Equal(t, m.Question, r.Question) -} - -func TestServer_Query_noMatch_domain(t *testing.T) { - addr := NewServer(). - Query("example.com. SOA", Noop). - Build(t) - - client := &dns.Client{Timeout: 1 * time.Second} - - m := new(dns.Msg).SetQuestion("example.org.", dns.TypeSOA) - - r, _, err := client.Exchange(m, addr.String()) - require.NoError(t, err) - - require.Equalf(t, dns.RcodeRefused, r.Rcode, - "expected %s, got %s", dns.RcodeToString[dns.RcodeRefused], dns.RcodeToString[r.Rcode]) - assert.Equal(t, m.Question, r.Question) -} - -func TestServer_Query_noMatch_type(t *testing.T) { - addr := NewServer(). - Query("example.com. SOA", Noop). - Build(t) - - client := &dns.Client{Timeout: 1 * time.Second} - - m := new(dns.Msg).SetQuestion("example.com.", dns.TypeTXT) - - r, _, err := client.Exchange(m, addr.String()) - require.NoError(t, err) - - require.Equalf(t, dns.RcodeNotImplemented, r.Rcode, - "expected %s, got %s", dns.RcodeToString[dns.RcodeNotImplemented], dns.RcodeToString[r.Rcode]) - assert.Equal(t, m.Question, r.Question) -} - -func TestServer_Query_noMatch_opType(t *testing.T) { - addr := NewServer(). - Query("example.com.", Noop). - Build(t) - - client := &dns.Client{Timeout: 1 * time.Second} - - m := new(dns.Msg).SetUpdate("example.com.") - m.Insert([]dns.RR{ - &dns.TXT{ - Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 1}, - Txt: []string{"foo"}, - }, - }) - - r, _, err := client.Exchange(m, addr.String()) - require.NoError(t, err) - - require.Equalf(t, dns.RcodeNotImplemented, r.Rcode, - "expected %s, got %s", dns.RcodeToString[dns.RcodeNotImplemented], dns.RcodeToString[r.Rcode]) - assert.Equal(t, m.Question, r.Question) -} - -func TestServer_Query_unknownType(t *testing.T) { - assert.PanicsWithValue(t, "QUERY: unknown type: ABC", func() { - NewServer(). - Query("example.com. ABC", Noop). - Build(t) - }) -} - -func TestServer_Query_duplicate(t *testing.T) { - assert.PanicsWithValue(t, "QUERY: duplicate route: example.com. SOA", func() { - NewServer(). - Query("example.com. SOA", Noop). - Query("example.com. SOA", Noop). - Build(t) - }) -} - -func TestServer_Query_duplicateGlobal(t *testing.T) { - assert.PanicsWithValue(t, "QUERY: a global route already exists for the domain: example.com.", func() { - NewServer(). - Query("example.com.", Noop). - Query("example.com.", Noop). - Build(t) - }) -} - -func TestServer_Query_mixed(t *testing.T) { - assert.PanicsWithValue(t, "QUERY: global route and specific routes cannot be mixed for the same domain: example.com.", func() { - NewServer(). - Query("example.com. SOA", Noop). - Query("example.com.", Noop). - Build(t) - }) -} - -func TestServer_Query_invalidDomain(t *testing.T) { - assert.PanicsWithValue(t, "QUERY: invalid domain: .example.com.", func() { - NewServer(). - Query(".example.com. SOA", Noop). - Build(t) - }) -} - -func TestServer_Query_invalidPattern(t *testing.T) { - assert.PanicsWithValue(t, "QUERY: invalid pattern: example.com. SOA 13", func() { - NewServer(). - Query("example.com. SOA 13", Noop). - Build(t) - }) -} - -func TestServer_Update(t *testing.T) { - addr := NewServer(). - Update("example.com.", Noop). - Build(t) - - client := &dns.Client{Timeout: 1 * time.Second} - - m := new(dns.Msg).SetUpdate("example.com.") - m.Insert([]dns.RR{ - &dns.TXT{ - Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 1}, - Txt: []string{"foo"}, - }, - }) - - r, _, err := client.Exchange(m, addr.String()) - require.NoError(t, err) - - require.Equalf(t, dns.RcodeSuccess, r.Rcode, - "expected %s, got %s", dns.RcodeToString[dns.RcodeSuccess], dns.RcodeToString[r.Rcode]) - assert.Equal(t, m.Question, r.Question) -} - -func TestServer_Update_noMatch_domain(t *testing.T) { - addr := NewServer(). - Update("example.com.", Noop). - Build(t) - - client := &dns.Client{Timeout: 1 * time.Second} - - m := new(dns.Msg).SetUpdate("example.org.") - m.Insert([]dns.RR{ - &dns.TXT{ - Hdr: dns.RR_Header{Name: "example.org.", Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 1}, - Txt: []string{"foo"}, - }, - }) - - r, _, err := client.Exchange(m, addr.String()) - require.NoError(t, err) - - require.Equalf(t, dns.RcodeRefused, r.Rcode, - "expected %s, got %s", dns.RcodeToString[dns.RcodeRefused], dns.RcodeToString[r.Rcode]) - assert.Equal(t, m.Question, r.Question) -} - -func TestServer_Update_noMatch_opType(t *testing.T) { - addr := NewServer(). - Update("example.com.", Noop). - Build(t) - - client := &dns.Client{Timeout: 1 * time.Second} - - m := new(dns.Msg).SetQuestion("example.com.", dns.TypeTXT) - - r, _, err := client.Exchange(m, addr.String()) - require.NoError(t, err) - - require.Equalf(t, dns.RcodeNotImplemented, r.Rcode, - "expected %s, got %s", dns.RcodeToString[dns.RcodeNotImplemented], dns.RcodeToString[r.Rcode]) - assert.Equal(t, m.Question, r.Question) -} - -func TestServer_Update_duplicate(t *testing.T) { - assert.PanicsWithValue(t, "UPDATE: a global route already exists for the domain: example.com.", func() { - NewServer(). - Update("example.com.", Noop). - Update("example.com.", Noop). - Build(t) - }) -} - -func TestServer_Update_invalidDomain(t *testing.T) { - assert.PanicsWithValue(t, "UPDATE: invalid domain: .example.com.", func() { - NewServer(). - Update(".example.com.", Noop). - Build(t) - }) -} - -func TestServer_Update_invalidPattern(t *testing.T) { - assert.PanicsWithValue(t, "UPDATE: invalid pattern: example.com. SOA 13", func() { - NewServer(). - Update("example.com. SOA 13", Noop). - Build(t) - }) -} diff --git a/platform/tester/dnsmock/handlers.go b/platform/tester/dnsmock/handlers.go deleted file mode 100644 index e1b047318..000000000 --- a/platform/tester/dnsmock/handlers.go +++ /dev/null @@ -1,76 +0,0 @@ -package dnsmock - -import ( - "fmt" - - "github.com/miekg/dns" -) - -func DumpRequest() dns.HandlerFunc { - return func(w dns.ResponseWriter, req *dns.Msg) { - fmt.Println(req) - - Noop(w, req) - } -} - -func SOA(name string) dns.HandlerFunc { - return func(w dns.ResponseWriter, req *dns.Msg) { - if name == "" { - name = req.Question[0].Name - } - - // Handle TLD - base := name - if dns.CountLabel(req.Question[0].Name) == 1 { - base = "nic." + req.Question[0].Name - } - - answer := &dns.SOA{ - Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: 120}, - Ns: "ns1." + base, - Mbox: "admin." + base, - Serial: 2016022801, - Refresh: 28800, - Retry: 7200, - Expire: 2419200, - Minttl: 1200, - } - - Answer(answer)(w, req) - } -} - -func CNAME(target string) dns.HandlerFunc { - return func(w dns.ResponseWriter, req *dns.Msg) { - answer := &dns.CNAME{ - Hdr: dns.RR_Header{Name: req.Question[0].Name, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: 1}, - Target: dns.Fqdn(target), - } - - Answer(answer)(w, req) - } -} - -func Noop(w dns.ResponseWriter, req *dns.Msg) { - _ = w.WriteMsg(new(dns.Msg).SetReply(req)) -} - -func Error(rcode int) dns.HandlerFunc { - return func(w dns.ResponseWriter, req *dns.Msg) { - _ = w.WriteMsg(new(dns.Msg).SetRcode(req, rcode)) - } -} - -func Answer(answer ...dns.RR) func(w dns.ResponseWriter, req *dns.Msg) { - return func(w dns.ResponseWriter, req *dns.Msg) { - m := new(dns.Msg).SetReply(req) - - m.Answer = answer - - err := w.WriteMsg(m) - if err != nil { - panic(err.Error()) - } - } -} diff --git a/platform/tester/dnsmock/handlers_test.go b/platform/tester/dnsmock/handlers_test.go deleted file mode 100644 index 13cdc0e2d..000000000 --- a/platform/tester/dnsmock/handlers_test.go +++ /dev/null @@ -1,156 +0,0 @@ -package dnsmock - -import ( - "testing" - "time" - - "github.com/miekg/dns" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestSOA_self(t *testing.T) { - addr := NewServer(). - Query("example.com. SOA", SOA("")). - Build(t) - - client := &dns.Client{Timeout: 1 * time.Second} - - m := new(dns.Msg).SetQuestion("example.com.", dns.TypeSOA) - - r, _, err := client.Exchange(m, addr.String()) - require.NoError(t, err) - - expectedSOA := []dns.RR{&dns.SOA{ - Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: 120, Rdlength: 56}, - Ns: "ns1.example.com.", - Mbox: "admin.example.com.", - Serial: 2016022801, - Refresh: 28800, - Retry: 7200, - Expire: 2419200, - Minttl: 1200, - }} - - require.Equal(t, dns.RcodeSuccess, r.Rcode) - assert.Equal(t, expectedSOA, r.Answer) - assert.Equal(t, m.Question, r.Question) -} - -func TestSOA_differentDomain(t *testing.T) { - addr := NewServer(). - Query("example.com. SOA", SOA("example.org.")). - Build(t) - - client := &dns.Client{Timeout: 1 * time.Second} - - m := new(dns.Msg).SetQuestion("example.com.", dns.TypeSOA) - - r, _, err := client.Exchange(m, addr.String()) - require.NoError(t, err) - - require.Equalf(t, dns.RcodeSuccess, r.Rcode, - "expected %s, got %s", dns.RcodeToString[dns.RcodeSuccess], dns.RcodeToString[r.Rcode]) - - expectedSOA := []dns.RR{&dns.SOA{ - Hdr: dns.RR_Header{Name: "example.org.", Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: 120, Rdlength: 56}, - Ns: "ns1.example.org.", - Mbox: "admin.example.org.", - Serial: 2016022801, - Refresh: 28800, - Retry: 7200, - Expire: 2419200, - Minttl: 1200, - }} - - assert.Equal(t, expectedSOA, r.Answer) - assert.Equal(t, m.Question, r.Question) -} - -func TestSOA_tld(t *testing.T) { - addr := NewServer(). - Query("com. SOA", SOA("")). - Build(t) - - client := &dns.Client{Timeout: 1 * time.Second} - - m := new(dns.Msg).SetQuestion("com.", dns.TypeSOA) - - r, _, err := client.Exchange(m, addr.String()) - require.NoError(t, err) - - require.Equalf(t, dns.RcodeSuccess, r.Rcode, - "expected %s, got %s", dns.RcodeToString[dns.RcodeSuccess], dns.RcodeToString[r.Rcode]) - - expectedSOA := []dns.RR{&dns.SOA{ - Hdr: dns.RR_Header{Name: "com.", Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: 120, Rdlength: 48}, - Ns: "ns1.nic.com.", - Mbox: "admin.nic.com.", - Serial: 2016022801, - Refresh: 28800, - Retry: 7200, - Expire: 2419200, - Minttl: 1200, - }} - - assert.Equal(t, expectedSOA, r.Answer) - assert.Equal(t, m.Question, r.Question) -} - -func TestCNAME(t *testing.T) { - addr := NewServer(). - Query("example.com. CNAME", CNAME("example.org.")). - Build(t) - - client := &dns.Client{Timeout: 1 * time.Second} - - m := new(dns.Msg).SetQuestion("example.com.", dns.TypeCNAME) - - r, _, err := client.Exchange(m, addr.String()) - require.NoError(t, err) - - require.Equalf(t, dns.RcodeSuccess, r.Rcode, - "expected %s, got %s", dns.RcodeToString[dns.RcodeSuccess], dns.RcodeToString[r.Rcode]) - - expectedCNAME := []dns.RR{&dns.CNAME{ - Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: 1, Rdlength: 13}, - Target: "example.org.", - }} - - assert.Equal(t, expectedCNAME, r.Answer) - assert.Equal(t, m.Question, r.Question) -} - -func TestNoop(t *testing.T) { - addr := NewServer(). - Query("example.com. CNAME", Noop). - Build(t) - - client := &dns.Client{Timeout: 1 * time.Second} - - m := new(dns.Msg).SetQuestion("example.com.", dns.TypeCNAME) - - r, _, err := client.Exchange(m, addr.String()) - require.NoError(t, err) - - require.Equalf(t, dns.RcodeSuccess, r.Rcode, - "expected %s, got %s", dns.RcodeToString[dns.RcodeSuccess], dns.RcodeToString[r.Rcode]) - assert.Equal(t, m.Question, r.Question) -} - -func TestError(t *testing.T) { - addr := NewServer(). - Query("example.com. CNAME", Error(dns.RcodeNameError)). - Build(t) - - client := &dns.Client{Timeout: 1 * time.Second} - - m := new(dns.Msg).SetQuestion("example.com.", dns.TypeCNAME) - - r, _, err := client.Exchange(m, addr.String()) - require.NoError(t, err) - - require.Equalf(t, dns.RcodeNameError, r.Rcode, - "expected %s, got %s", dns.RcodeToString[dns.RcodeNameError], dns.RcodeToString[r.Rcode]) - assert.Equal(t, m.Question, r.Question) -} diff --git a/platform/tester/env.go b/platform/tester/env.go index a12c32ef8..26788be3b 100644 --- a/platform/tester/env.go +++ b/platform/tester/env.go @@ -21,7 +21,6 @@ type EnvTest struct { // NewEnvTest Creates an EnvTest. func NewEnvTest(keys ...string) *EnvTest { values := make(map[string]string) - for _, key := range keys { value := os.Getenv(key) if value != "" { @@ -40,7 +39,6 @@ func NewEnvTest(keys ...string) *EnvTest { func (e *EnvTest) WithDomain(key string) *EnvTest { e.domainKey = key e.domain = os.Getenv(key) - return e } diff --git a/platform/tester/env_test.go b/platform/tester/env_test.go index 4d9e4e7d1..25748f8ff 100644 --- a/platform/tester/env_test.go +++ b/platform/tester/env_test.go @@ -18,7 +18,6 @@ const ( func TestMain(m *testing.M) { exitCode := m.Run() - clearEnv() os.Exit(exitCode) } @@ -40,7 +39,6 @@ func clearEnv() { os.Unsetenv(strings.Split(key, "=")[0]) } } - os.Unsetenv("EXTRA_LEGO_TEST") } @@ -64,7 +62,7 @@ func TestEnvTest(t *testing.T) { assert.True(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) - assert.Empty(t, envTest.GetDomain()) + assert.Equal(t, "", envTest.GetDomain()) }, }, { @@ -77,9 +75,9 @@ func TestEnvTest(t *testing.T) { }, expected: func(t *testing.T, envTest *tester.EnvTest) { assert.False(t, envTest.IsLiveTest()) - assert.Empty(t, envTest.GetValue(envVar01)) + assert.Equal(t, "", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) - assert.Empty(t, envTest.GetDomain()) + assert.Equal(t, "", envTest.GetDomain()) }, }, { @@ -96,7 +94,7 @@ func TestEnvTest(t *testing.T) { assert.True(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) - assert.Empty(t, envTest.GetValue(envVarDomain)) + assert.Equal(t, "", envTest.GetValue(envVarDomain)) assert.Equal(t, "D", envTest.GetDomain()) }, }, @@ -112,8 +110,8 @@ func TestEnvTest(t *testing.T) { expected: func(t *testing.T, envTest *tester.EnvTest) { assert.False(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) - assert.Empty(t, envTest.GetValue(envVar02)) - assert.Empty(t, envTest.GetValue(envVarDomain)) + assert.Equal(t, "", envTest.GetValue(envVar02)) + assert.Equal(t, "", envTest.GetValue(envVarDomain)) assert.Equal(t, "D", envTest.GetDomain()) }, }, @@ -130,8 +128,8 @@ func TestEnvTest(t *testing.T) { assert.False(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) - assert.Empty(t, envTest.GetValue(envVarDomain)) - assert.Empty(t, envTest.GetDomain()) + assert.Equal(t, "", envTest.GetValue(envVarDomain)) + assert.Equal(t, "", envTest.GetDomain()) }, }, { @@ -147,7 +145,7 @@ func TestEnvTest(t *testing.T) { assert.True(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) - assert.Empty(t, envTest.GetDomain()) + assert.Equal(t, "", envTest.GetDomain()) }, }, { @@ -163,7 +161,7 @@ func TestEnvTest(t *testing.T) { assert.True(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) - assert.Empty(t, envTest.GetDomain()) + assert.Equal(t, "", envTest.GetDomain()) }, }, { @@ -176,9 +174,9 @@ func TestEnvTest(t *testing.T) { }, expected: func(t *testing.T, envTest *tester.EnvTest) { assert.True(t, envTest.IsLiveTest()) - assert.Empty(t, envTest.GetValue(envVar01)) + assert.Equal(t, "", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) - assert.Empty(t, envTest.GetDomain()) + assert.Equal(t, "", envTest.GetDomain()) }, }, { @@ -192,8 +190,8 @@ func TestEnvTest(t *testing.T) { expected: func(t *testing.T, envTest *tester.EnvTest) { assert.False(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) - assert.Empty(t, envTest.GetValue(envVar02)) - assert.Empty(t, envTest.GetDomain()) + assert.Equal(t, "", envTest.GetValue(envVar02)) + assert.Equal(t, "", envTest.GetDomain()) }, }, { @@ -212,7 +210,7 @@ func TestEnvTest(t *testing.T) { assert.True(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) - assert.Empty(t, envTest.GetValue(envVarDomain)) + assert.Equal(t, "", envTest.GetValue(envVarDomain)) assert.Equal(t, "D", envTest.GetDomain()) }, }, @@ -231,8 +229,8 @@ func TestEnvTest(t *testing.T) { assert.True(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) - assert.Empty(t, envTest.GetValue(envVarDomain)) - assert.Empty(t, envTest.GetDomain()) + assert.Equal(t, "", envTest.GetValue(envVarDomain)) + assert.Equal(t, "", envTest.GetDomain()) }, }, { @@ -249,7 +247,7 @@ func TestEnvTest(t *testing.T) { assert.True(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) - assert.Empty(t, envTest.GetDomain()) + assert.Equal(t, "", envTest.GetDomain()) }, }, { @@ -266,7 +264,7 @@ func TestEnvTest(t *testing.T) { assert.False(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) - assert.Empty(t, envTest.GetDomain()) + assert.Equal(t, "", envTest.GetDomain()) }, }, { @@ -284,7 +282,7 @@ func TestEnvTest(t *testing.T) { assert.True(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) - assert.Empty(t, envTest.GetDomain()) + assert.Equal(t, "", envTest.GetDomain()) }, }, { @@ -302,7 +300,7 @@ func TestEnvTest(t *testing.T) { assert.False(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) - assert.Empty(t, envTest.GetDomain()) + assert.Equal(t, "", envTest.GetDomain()) }, }, { @@ -318,8 +316,8 @@ func TestEnvTest(t *testing.T) { expected: func(t *testing.T, envTest *tester.EnvTest) { assert.False(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) - assert.Empty(t, envTest.GetValue(envVar02)) - assert.Empty(t, envTest.GetDomain()) + assert.Equal(t, "", envTest.GetValue(envVar02)) + assert.Equal(t, "", envTest.GetDomain()) }, }, } @@ -327,7 +325,6 @@ func TestEnvTest(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer clearEnv() - applyEnv(test.envVars) envTest := test.envTestSetup() @@ -360,7 +357,7 @@ func TestEnvTest_ClearEnv(t *testing.T) { envTest.ClearEnv() - assert.Empty(t, os.Getenv(envVar01)) - assert.Empty(t, os.Getenv(envVar02)) + assert.Equal(t, "", os.Getenv(envVar01)) + assert.Equal(t, "", os.Getenv(envVar02)) assert.Equal(t, "X", os.Getenv("EXTRA_LEGO_TEST")) } diff --git a/platform/tester/servermock/builder.go b/platform/tester/servermock/builder.go deleted file mode 100644 index b5a9d909b..000000000 --- a/platform/tester/servermock/builder.go +++ /dev/null @@ -1,84 +0,0 @@ -package servermock - -import ( - "net/http" - "net/http/httptest" - "slices" - "testing" - - "github.com/stretchr/testify/require" -) - -// Link represents a middleware interface, enabling middleware chaining. -type Link interface { - Bind(next http.Handler) http.Handler -} - -// LinkFunc defines a function type [Link]. -type LinkFunc func(next http.Handler) http.Handler - -func (f LinkFunc) Bind(next http.Handler) http.Handler { - return f(next) -} - -// ClientBuilder defines a function type for creating a client of type T based on a httptest.Server instance. -type ClientBuilder[T any] func(server *httptest.Server) (T, error) - -// Builder is a type that facilitates the construction of testable HTTP clients and server. -// It allows defining routes, attaching middleware, and creating custom HTTP clients. -type Builder[T any] struct { - mux *http.ServeMux - chain []Link - - clientBuilder ClientBuilder[T] -} - -func NewBuilder[T any](clientBuilder ClientBuilder[T], chain ...Link) *Builder[T] { - return &Builder[T]{ - mux: http.NewServeMux(), - chain: chain, - clientBuilder: clientBuilder, - } -} - -func (b *Builder[T]) Route(pattern string, handler http.Handler, chain ...Link) *Builder[T] { - if handler == nil { - handler = Noop() - } - - for _, link := range slices.Backward(b.chain) { - handler = link.Bind(handler) - } - - for _, link := range slices.Backward(chain) { - handler = link.Bind(handler) - } - - b.mux.Handle(pattern, handler) - - return b -} - -func (b *Builder[T]) Build(t *testing.T) T { - t.Helper() - - server := httptest.NewServer(b.mux) - t.Cleanup(server.Close) - - client, err := b.clientBuilder(server) - require.NoError(t, err) - - return client -} - -func (b *Builder[T]) BuildHTTPS(t *testing.T) T { - t.Helper() - - server := httptest.NewTLSServer(b.mux) - t.Cleanup(server.Close) - - client, err := b.clientBuilder(server) - require.NoError(t, err) - - return client -} diff --git a/platform/tester/servermock/handler_dump.go b/platform/tester/servermock/handler_dump.go deleted file mode 100644 index 83f902980..000000000 --- a/platform/tester/servermock/handler_dump.go +++ /dev/null @@ -1,20 +0,0 @@ -package servermock - -import ( - "fmt" - "net/http" - "net/http/httputil" -) - -// DumpRequest logs the full HTTP request to the console, including the body if present. -func DumpRequest() http.HandlerFunc { - return func(rw http.ResponseWriter, req *http.Request) { - dump, err := httputil.DumpRequest(req, true) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - - fmt.Println(string(dump)) - } -} diff --git a/platform/tester/servermock/handler_file.go b/platform/tester/servermock/handler_file.go deleted file mode 100644 index c5a9b33e1..000000000 --- a/platform/tester/servermock/handler_file.go +++ /dev/null @@ -1,84 +0,0 @@ -package servermock - -import ( - "io" - "net/http" - "os" - "path/filepath" - "slices" -) - -// ResponseFromFileHandler handles HTTP responses using the content of a file. -type ResponseFromFileHandler struct { - statusCode int - headers http.Header - filename string -} - -// ResponseFromFile creates a [ResponseFromFileHandler] using a filename. -func ResponseFromFile(filename string) *ResponseFromFileHandler { - return &ResponseFromFileHandler{ - statusCode: http.StatusOK, - headers: http.Header{}, - filename: filename, - } -} - -// ResponseFromFixture creates a [ResponseFromFileHandler] using a filename from the `fixtures` directory. -func ResponseFromFixture(filename string) *ResponseFromFileHandler { - return ResponseFromFile(filepath.Join("fixtures", filename)) -} - -// ResponseFromInternal creates a [ResponseFromFileHandler] using a filename from the `internal/fixtures` directory. -func ResponseFromInternal(filename string) *ResponseFromFileHandler { - return ResponseFromFile(filepath.Join("internal", "fixtures", filename)) -} - -func (h *ResponseFromFileHandler) ServeHTTP(rw http.ResponseWriter, _ *http.Request) { - for k, values := range h.headers { - for _, v := range values { - rw.Header().Add(k, v) - } - } - - if h.filename == "" { - rw.WriteHeader(h.statusCode) - return - } - - if filepath.Ext(h.filename) == ".json" { - rw.Header().Set(contentTypeHeader, applicationJSONMimeType) - } - - file, err := os.Open(h.filename) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - - defer func() { _ = file.Close() }() - - rw.WriteHeader(h.statusCode) - - _, err = io.Copy(rw, file) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } -} - -func (h *ResponseFromFileHandler) WithStatusCode(status int) *ResponseFromFileHandler { - if h.statusCode >= http.StatusContinue { - h.statusCode = status - } - - return h -} - -func (h *ResponseFromFileHandler) WithHeader(name, value string, values ...string) *ResponseFromFileHandler { - for _, v := range slices.Concat([]string{value}, values) { - h.headers.Add(name, v) - } - - return h -} diff --git a/platform/tester/servermock/handler_json.go b/platform/tester/servermock/handler_json.go deleted file mode 100644 index f1c2aa9ce..000000000 --- a/platform/tester/servermock/handler_json.go +++ /dev/null @@ -1,39 +0,0 @@ -package servermock - -import ( - "encoding/json" - "net/http" -) - -// JSONEncodeHandler is a handler that encodes data into JSON and writes it to an HTTP response. -type JSONEncodeHandler struct { - data any - statusCode int -} - -func JSONEncode(data any) *JSONEncodeHandler { - return &JSONEncodeHandler{ - data: data, - statusCode: http.StatusOK, - } -} - -func (h *JSONEncodeHandler) ServeHTTP(rw http.ResponseWriter, _ *http.Request) { - rw.Header().Set(contentTypeHeader, applicationJSONMimeType) - - rw.WriteHeader(h.statusCode) - - err := json.NewEncoder(rw).Encode(h.data) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } -} - -func (h *JSONEncodeHandler) WithStatusCode(status int) *JSONEncodeHandler { - if h.statusCode >= http.StatusContinue { - h.statusCode = status - } - - return h -} diff --git a/platform/tester/servermock/handler_noop.go b/platform/tester/servermock/handler_noop.go deleted file mode 100644 index 6df5164e6..000000000 --- a/platform/tester/servermock/handler_noop.go +++ /dev/null @@ -1,45 +0,0 @@ -package servermock - -import ( - "net/http" - "slices" -) - -// NoopHandler is a simple HTTP handler that responds without processing requests. -type NoopHandler struct { - statusCode int - headers http.Header -} - -func Noop() *NoopHandler { - return &NoopHandler{ - statusCode: http.StatusOK, - headers: http.Header{}, - } -} - -func (h *NoopHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - for k, values := range h.headers { - for _, v := range values { - rw.Header().Add(k, v) - } - } - - rw.WriteHeader(h.statusCode) -} - -func (h *NoopHandler) WithStatusCode(status int) *NoopHandler { - if h.statusCode >= http.StatusContinue { - h.statusCode = status - } - - return h -} - -func (h *NoopHandler) WithHeader(name, value string, values ...string) *NoopHandler { - for _, v := range slices.Concat([]string{value}, values) { - h.headers.Add(name, v) - } - - return h -} diff --git a/platform/tester/servermock/handler_raw.go b/platform/tester/servermock/handler_raw.go deleted file mode 100644 index d7c68f396..000000000 --- a/platform/tester/servermock/handler_raw.go +++ /dev/null @@ -1,61 +0,0 @@ -package servermock - -import ( - "net/http" - "slices" -) - -// RawResponseHandler is a custom HTTP handler that serves raw response data. -type RawResponseHandler struct { - statusCode int - headers http.Header - data []byte -} - -func RawResponse(data []byte) *RawResponseHandler { - return &RawResponseHandler{ - statusCode: http.StatusOK, - headers: http.Header{}, - data: data, - } -} - -func RawStringResponse(data string) *RawResponseHandler { - return RawResponse([]byte(data)) -} - -func (h *RawResponseHandler) ServeHTTP(rw http.ResponseWriter, _ *http.Request) { - for k, values := range h.headers { - for _, v := range values { - rw.Header().Add(k, v) - } - } - - rw.WriteHeader(h.statusCode) - - if len(h.data) == 0 { - return - } - - _, err := rw.Write(h.data) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } -} - -func (h *RawResponseHandler) WithStatusCode(status int) *RawResponseHandler { - if h.statusCode >= http.StatusContinue { - h.statusCode = status - } - - return h -} - -func (h *RawResponseHandler) WithHeader(name, value string, values ...string) *RawResponseHandler { - for _, v := range slices.Concat([]string{value}, values) { - h.headers.Add(name, v) - } - - return h -} diff --git a/platform/tester/servermock/link_form.go b/platform/tester/servermock/link_form.go deleted file mode 100644 index 581e27d66..000000000 --- a/platform/tester/servermock/link_form.go +++ /dev/null @@ -1,97 +0,0 @@ -package servermock - -import ( - "fmt" - "net/http" - "net/url" - "regexp" - "slices" -) - -// FormLink is a type used for validating and processing form data in HTTP requests. -// It supports strict validation, predefined values, and regex-based checks to ensure form compliance. -type FormLink struct { - values url.Values - regexes map[string]*regexp.Regexp - strict bool - usePostForm bool - statusCode int -} - -func CheckForm() *FormLink { - return &FormLink{ - values: url.Values{}, - regexes: map[string]*regexp.Regexp{}, - statusCode: http.StatusBadRequest, - } -} - -func (l *FormLink) Bind(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - err := req.ParseForm() - if err != nil { - http.Error(rw, err.Error(), l.statusCode) - return - } - - form := req.Form - if l.usePostForm { - form = req.PostForm - } - - if l.strict { - if len(form) != len(l.values)+len(l.regexes) { - msg := fmt.Sprintf("invalid query parameters, got %v, want %v", req.Form, l.values) - http.Error(rw, msg, l.statusCode) - - return - } - } - - for k, v := range l.values { - value := form[k] - if !slices.Equal(v, value) { - msg := fmt.Sprintf("invalid %q form value, got %q, want %q", k, value, v) - http.Error(rw, msg, l.statusCode) - - return - } - } - - for k, exp := range l.regexes { - value := form.Get(k) - if !exp.MatchString(value) { - msg := fmt.Sprintf("invalid %q form value, %q doesn't match to %q", k, value, exp) - http.Error(rw, msg, l.statusCode) - - return - } - } - - next.ServeHTTP(rw, req) - }) -} - -func (l *FormLink) Strict() *FormLink { - l.strict = true - - return l -} - -func (l *FormLink) UsePostForm() *FormLink { - l.usePostForm = true - - return l -} - -func (l *FormLink) With(name, value string) *FormLink { - l.values.Set(name, value) - - return l -} - -func (l *FormLink) WithRegexp(name, exp string) *FormLink { - l.regexes[name] = regexp.MustCompile(exp) - - return l -} diff --git a/platform/tester/servermock/link_headers.go b/platform/tester/servermock/link_headers.go deleted file mode 100644 index 0ca519958..000000000 --- a/platform/tester/servermock/link_headers.go +++ /dev/null @@ -1,178 +0,0 @@ -package servermock - -import ( - "fmt" - "net/http" - "regexp" - "slices" -) - -const ( - authorizationHeader = "Authorization" - contentTypeHeader = "Content-Type" - acceptHeader = "Accept" -) - -const ( - applicationJSONMimeType = "application/json" - applicationFormMimeType = "application/x-www-form-urlencoded" -) - -type basicAuth struct { - username, password string -} - -// HeaderLink validates HTTP request headers. -type HeaderLink struct { - values http.Header - regexes map[string]*regexp.Regexp - json bool - basicAuth *basicAuth - statusCode int -} - -func CheckHeader() *HeaderLink { - return &HeaderLink{ - values: http.Header{}, - regexes: map[string]*regexp.Regexp{}, - statusCode: http.StatusBadRequest, - } -} - -func (l *HeaderLink) Bind(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - for k, v := range l.values { - err := checkHeader(req, k, v) - if err != nil { - http.Error(rw, err.Error(), l.statusCode) - return - } - } - - for k, exp := range l.regexes { - value := req.Header.Get(k) - - if !exp.MatchString(value) { - msg := fmt.Sprintf("invalid %q header value, %q doesn't match to %q", k, value, exp) - http.Error(rw, msg, l.statusCode) - - return - } - } - - if l.json && !l.checkJSONHeaders(rw, req) { - return - } - - if l.basicAuth != nil && !l.checkBasicAuth(rw, req) { - return - } - - next.ServeHTTP(rw, req) - }) -} - -func (l *HeaderLink) With(name, value string, values ...string) *HeaderLink { - for _, v := range slices.Concat([]string{value}, values) { - l.values.Add(name, v) - } - - return l -} - -func (l *HeaderLink) WithRegexp(name, exp string) *HeaderLink { - l.regexes[name] = regexp.MustCompile(exp) - - return l -} - -func (l *HeaderLink) WithJSONHeaders() *HeaderLink { - l.json = true - - return l -} - -func (l *HeaderLink) WithContentTypeFromURLEncoded() *HeaderLink { - l.values.Set(contentTypeHeader, applicationFormMimeType) - - return l -} - -func (l *HeaderLink) WithContentType(value string) *HeaderLink { - l.values.Set(contentTypeHeader, value) - - return l -} - -func (l *HeaderLink) WithAccept(value string) *HeaderLink { - l.values.Set(acceptHeader, value) - - return l -} - -func (l *HeaderLink) WithAuthorization(value string) *HeaderLink { - l.values.Set(authorizationHeader, value) - - return l -} - -func (l *HeaderLink) WithStatusCode(status int) *HeaderLink { - if l.statusCode >= http.StatusContinue { - l.statusCode = status - } - - return l -} - -func (l *HeaderLink) WithBasicAuth(username, password string) *HeaderLink { - l.basicAuth = &basicAuth{username: username, password: password} - - return l -} - -func (l *HeaderLink) checkBasicAuth(rw http.ResponseWriter, req *http.Request) bool { - usr, pwd, ok := req.BasicAuth() - if !ok { - http.Error(rw, "missing Basic auth", l.statusCode) - - return false - } - - if usr != l.basicAuth.username || pwd != l.basicAuth.password { - msg := fmt.Sprintf("invalid credentials: got [username: %q, password: %q], want [username: %q, password: %q]", - usr, pwd, l.basicAuth.username, l.basicAuth.password) - http.Error(rw, msg, l.statusCode) - - return false - } - - return true -} - -func (l *HeaderLink) checkJSONHeaders(rw http.ResponseWriter, req *http.Request) bool { - err := checkHeader(req, acceptHeader, []string{applicationJSONMimeType}) - if err != nil { - http.Error(rw, err.Error(), l.statusCode) - - return false - } - - if req.ContentLength > 0 { - err = checkHeader(req, contentTypeHeader, []string{applicationJSONMimeType}) - if err != nil { - http.Error(rw, err.Error(), l.statusCode) - - return false - } - } - - return true -} - -func checkHeader(req *http.Request, k string, v []string) error { - if !slices.Equal(req.Header[k], v) { - return fmt.Errorf("invalid %q header value, got %q, want %q", k, req.Header[k], v) - } - - return nil -} diff --git a/platform/tester/servermock/link_query.go b/platform/tester/servermock/link_query.go deleted file mode 100644 index 14f776515..000000000 --- a/platform/tester/servermock/link_query.go +++ /dev/null @@ -1,100 +0,0 @@ -package servermock - -import ( - "fmt" - "net/http" - "net/url" - "regexp" -) - -// QueryParameterLink validates query parameters in HTTP requests. -// The strict flag enforces exact matches with specified query parameters. -type QueryParameterLink struct { - values map[string]string - regexes map[string]*regexp.Regexp - strict bool - statusCode int -} - -func CheckQueryParameter() *QueryParameterLink { - return &QueryParameterLink{ - values: map[string]string{}, - regexes: map[string]*regexp.Regexp{}, - statusCode: http.StatusBadRequest, - } -} - -func (l *QueryParameterLink) Bind(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - query := req.URL.Query() - - if l.strict { - if len(query) != len(l.values)+len(l.regexes) { - msg := fmt.Sprintf("invalid query parameters, got %v, want %v", query, l.values) - http.Error(rw, msg, l.statusCode) - - return - } - } - - for k, v := range l.values { - p := query.Get(k) - if p != v { - msg := fmt.Sprintf("invalid %q query parameter value, got %q, want %q", k, p, v) - http.Error(rw, msg, l.statusCode) - - return - } - } - - for k, exp := range l.regexes { - value := query.Get(k) - if !exp.MatchString(value) { - msg := fmt.Sprintf("invalid %q query parameter value, %q doesn't match to %q", k, value, exp) - http.Error(rw, msg, l.statusCode) - - return - } - } - - next.ServeHTTP(rw, req) - }) -} - -func (l *QueryParameterLink) Strict() *QueryParameterLink { - l.strict = true - - return l -} - -func (l *QueryParameterLink) With(name, value string) *QueryParameterLink { - l.values[name] = value - - return l -} - -func (l *QueryParameterLink) WithRegexp(name, exp string) *QueryParameterLink { - l.regexes[name] = regexp.MustCompile(exp) - - return l -} - -func (l *QueryParameterLink) WithValues(values url.Values) *QueryParameterLink { - for k, v := range values { - if len(v) != 1 { - continue - } - - l.values[k] = v[0] - } - - return l -} - -func (l *QueryParameterLink) WithStatusCode(status int) *QueryParameterLink { - if l.statusCode >= http.StatusContinue { - l.statusCode = status - } - - return l -} diff --git a/platform/tester/servermock/link_request_body.go b/platform/tester/servermock/link_request_body.go deleted file mode 100644 index d6b2d9efd..000000000 --- a/platform/tester/servermock/link_request_body.go +++ /dev/null @@ -1,100 +0,0 @@ -package servermock - -import ( - "bytes" - "fmt" - "io" - "net/http" - "os" - "path/filepath" - "slices" -) - -// RequestBodyLink represents a handler utility to validate HTTP request bodies against a predefined byte slice. -type RequestBodyLink struct { - body []byte - filename string - ignoreWhitespace bool -} - -// CheckRequestBody creates a [RequestBodyLink] initialized with the provided request body string. -func CheckRequestBody(body string) *RequestBodyLink { - return &RequestBodyLink{body: []byte(body)} -} - -// CheckRequestBodyFromFile creates a [RequestBodyLink] initialized with the provided request body file. -func CheckRequestBodyFromFile(filename string) *RequestBodyLink { - return &RequestBodyLink{filename: filename} -} - -// CheckRequestBodyFromFixture creates a [RequestBodyLink] initialized with the provided request body file from the `fixtures` directory. -func CheckRequestBodyFromFixture(filename string) *RequestBodyLink { - return CheckRequestBodyFromFile(filepath.Join("fixtures", filename)) -} - -// CheckRequestBodyFromInternal creates a [RequestBodyLink] initialized with the provided request body file from the `internal/fixtures directory. -func CheckRequestBodyFromInternal(filename string) *RequestBodyLink { - return CheckRequestBodyFromFile(filepath.Join("internal", "fixtures", filename)) -} - -func (l *RequestBodyLink) Bind(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - if req.ContentLength == 0 { - http.Error(rw, fmt.Sprintf("%s: empty request body", req.URL.Path), http.StatusBadRequest) - return - } - - body, err := io.ReadAll(req.Body) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - - _ = req.Body.Close() - - expectedRaw := slices.Clone(l.body) - - if l.filename != "" { - expectedRaw, err = os.ReadFile(l.filename) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - } - - if len(expectedRaw) == 0 { - http.Error(rw, fmt.Sprintf("%s: empty expected request body", req.URL.Path), http.StatusBadRequest) - return - } - - if l.ignoreWhitespace { - body = trimLineSpace(body) - expectedRaw = trimLineSpace(expectedRaw) - } - - if !bytes.Equal(bytes.TrimSpace(expectedRaw), bytes.TrimSpace(body)) { - msg := fmt.Sprintf("%s: request body differences: got: %s, want: %s", req.URL.Path, - string(bytes.TrimSpace(body)), string(bytes.TrimSpace(expectedRaw))) - http.Error(rw, msg, http.StatusBadRequest) - - return - } - - next.ServeHTTP(rw, req) - }) -} - -func (l *RequestBodyLink) IgnoreWhitespace() *RequestBodyLink { - l.ignoreWhitespace = true - - return l -} - -func trimLineSpace(body []byte) []byte { - buf := bytes.NewBuffer(nil) - for line := range bytes.Lines(body) { - buf.Write(bytes.TrimSpace(line)) - } - - return buf.Bytes() -} diff --git a/platform/tester/servermock/link_request_body_json.go b/platform/tester/servermock/link_request_body_json.go deleted file mode 100644 index ed5a117ba..000000000 --- a/platform/tester/servermock/link_request_body_json.go +++ /dev/null @@ -1,114 +0,0 @@ -package servermock - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "path/filepath" - "slices" - - "github.com/google/go-cmp/cmp" -) - -// RequestBodyJSONLink validates JSON request bodies. -type RequestBodyJSONLink struct { - body []byte - filename string - data any -} - -// CheckRequestJSONBody creates a [RequestBodyJSONLink] initialized with a string. -func CheckRequestJSONBody(body string) *RequestBodyJSONLink { - return &RequestBodyJSONLink{body: []byte(body)} -} - -// CheckRequestJSONBodyFromStruct creates a [RequestBodyJSONLink] initialized with a struct. -func CheckRequestJSONBodyFromStruct(data any) *RequestBodyJSONLink { - return &RequestBodyJSONLink{data: data} -} - -// CheckRequestJSONBodyFromFile creates a [RequestBodyJSONLink] initialized with the provided request body file. -func CheckRequestJSONBodyFromFile(filename string) *RequestBodyJSONLink { - return &RequestBodyJSONLink{ - filename: filename, - } -} - -// CheckRequestJSONBodyFromFixture creates a [RequestBodyJSONLink] initialized with the provided request body file from the `fixtures` directory. -func CheckRequestJSONBodyFromFixture(filename string) *RequestBodyJSONLink { - return CheckRequestJSONBodyFromFile(filepath.Join("fixtures", filename)) -} - -// CheckRequestJSONBodyFromInternal creates a [RequestBodyJSONLink] initialized with the provided request body file from the `internal/fixtures` directory. -func CheckRequestJSONBodyFromInternal(filename string) *RequestBodyJSONLink { - return CheckRequestJSONBodyFromFile(filepath.Join("internal", "fixtures", filename)) -} - -func (l *RequestBodyJSONLink) Bind(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - if req.ContentLength == 0 { - http.Error(rw, fmt.Sprintf("%s: empty request body", req.URL.Path), http.StatusBadRequest) - return - } - - body, err := io.ReadAll(req.Body) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - - _ = req.Body.Close() - - var expected, actual any - - expectedRaw := slices.Clone(l.body) - - switch { - case l.filename != "": - expectedRaw, err = os.ReadFile(l.filename) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - - case l.data != nil: - expectedRaw, err = json.Marshal(l.data) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - } - - if len(expectedRaw) == 0 { - http.Error(rw, fmt.Sprintf("%s: empty expected request body", req.URL.Path), http.StatusBadRequest) - return - } - - err = json.Unmarshal(expectedRaw, &expected) - if err != nil { - msg := fmt.Sprintf("%s: the expected request body is not valid JSON: %v", req.URL.Path, err) - http.Error(rw, msg, http.StatusBadRequest) - - return - } - - err = json.Unmarshal(body, &actual) - if err != nil { - msg := fmt.Sprintf("%s: request body is not valid JSON: %v", req.URL.Path, err) - http.Error(rw, msg, http.StatusBadRequest) - - return - } - - if !cmp.Equal(actual, expected) { - msg := fmt.Sprintf("%s: request body differences: %s", req.URL.Path, cmp.Diff(actual, expected)) - http.Error(rw, msg, http.StatusBadRequest) - - return - } - - next.ServeHTTP(rw, req) - }) -} diff --git a/platform/wait/wait.go b/platform/wait/wait.go index c66f57446..6ad817b26 100644 --- a/platform/wait/wait.go +++ b/platform/wait/wait.go @@ -1,11 +1,9 @@ package wait import ( - "context" "fmt" "time" - "github.com/cenkalti/backoff/v5" "github.com/go-acme/lego/v4/log" ) @@ -14,25 +12,21 @@ func For(msg string, timeout, interval time.Duration, f func() (bool, error)) er log.Infof("Wait for %s [timeout: %s, interval: %s]", msg, timeout, interval) var lastErr error - timeUp := time.After(timeout) - for { select { case <-timeUp: if lastErr == nil { return fmt.Errorf("%s: time limit exceeded", msg) } - return fmt.Errorf("%s: time limit exceeded: last error: %w", msg, lastErr) default: } stop, err := f() if stop { - return err + return nil } - if err != nil { lastErr = err } @@ -40,13 +34,3 @@ func For(msg string, timeout, interval time.Duration, f func() (bool, error)) er time.Sleep(interval) } } - -// Retry retries the given operation until it succeeds or the context is canceled. -// Similar to [backoff.Retry] but with a different signature. -func Retry(ctx context.Context, operation func() error, opts ...backoff.RetryOption) error { - _, err := backoff.Retry(ctx, func() (any, error) { - return nil, operation() - }, opts...) - - return err -} diff --git a/platform/wait/wait_test.go b/platform/wait/wait_test.go index 36dbffe69..9722e6f2e 100644 --- a/platform/wait/wait_test.go +++ b/platform/wait/wait_test.go @@ -1,121 +1,26 @@ package wait import ( - "errors" - "sync/atomic" "testing" "time" - - "github.com/stretchr/testify/require" ) -// TODO(ldez): rewrite those tests when upgrading to go1.25 as minimum Go version. - -func TestFor_timeout(t *testing.T) { - var io atomic.Int64 - +func TestForTimeout(t *testing.T) { c := make(chan error) - go func() { - c <- For("test", 3*time.Second, 1*time.Second, func() (bool, error) { - io.Add(1) - - if io.Load() == 1 { - return false, nil - } - + c <- For("", 3*time.Second, 1*time.Second, func() (bool, error) { return false, nil }) }() timeout := time.After(6 * time.Second) - select { case <-timeout: t.Fatal("timeout exceeded") case err := <-c: - require.EqualError(t, err, "test: time limit exceeded") + if err == nil { + t.Errorf("expected timeout error; got %v", err) + } + t.Logf("%v", err) } - - require.EqualValues(t, 3, io.Load()) -} - -func TestFor_timeout_with_error(t *testing.T) { - var io atomic.Int64 - - c := make(chan error) - - go func() { - c <- For("test", 3*time.Second, 1*time.Second, func() (bool, error) { - io.Add(1) - - // This allows be sure that the latest previous error is returned. - if io.Load() == 1 { - return false, errors.New("oops") - } - - return false, nil - }) - }() - - timeout := time.After(6 * time.Second) - - select { - case <-timeout: - t.Fatal("timeout exceeded") - case err := <-c: - require.EqualError(t, err, "test: time limit exceeded: last error: oops") - } - - require.EqualValues(t, 3, io.Load()) -} - -func TestFor_stop(t *testing.T) { - var io atomic.Int64 - - c := make(chan error) - - go func() { - c <- For("test", 3*time.Second, 1*time.Second, func() (bool, error) { - io.Add(1) - - return true, nil - }) - }() - - timeout := time.After(6 * time.Second) - - select { - case <-timeout: - t.Fatal("timeout exceeded") - case err := <-c: - require.NoError(t, err) - } - - require.EqualValues(t, 1, io.Load()) -} - -func TestFor_stop_with_error(t *testing.T) { - var io atomic.Int64 - - c := make(chan error) - - go func() { - c <- For("test", 3*time.Second, 1*time.Second, func() (bool, error) { - io.Add(1) - - return true, errors.New("oops") - }) - }() - - timeout := time.After(6 * time.Second) - - select { - case <-timeout: - t.Fatal("timeout exceeded") - case err := <-c: - require.EqualError(t, err, "oops") - } - - require.EqualValues(t, 1, io.Load()) } diff --git a/providers/dns/acmedns/acmedns.go b/providers/dns/acmedns/acmedns.go index 8f1f16842..b7ec31653 100644 --- a/providers/dns/acmedns/acmedns.go +++ b/providers/dns/acmedns/acmedns.go @@ -3,17 +3,12 @@ package acmedns import ( - "context" "errors" "fmt" - "strings" - "github.com/go-acme/lego/v4/challenge" + "github.com/cpu/goacmedns" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/acmedns/internal" - "github.com/nrdcg/goacmedns" - "github.com/nrdcg/goacmedns/storage" ) const ( @@ -23,112 +18,54 @@ const ( // EnvAPIBase is the environment variable name for the ACME-DNS API address. // (e.g. https://acmedns.your-domain.com). EnvAPIBase = envNamespace + "API_BASE" - - // EnvAllowList are source networks using CIDR notation, - // e.g. "192.168.100.1/24,1.2.3.4/32,2002:c0a8:2a00::0/40". - EnvAllowList = envNamespace + "ALLOWLIST" - // EnvStoragePath is the environment variable name for the ACME-DNS JSON account data file. // A per-domain account will be registered/persisted to this file and used for TXT updates. EnvStoragePath = envNamespace + "STORAGE_PATH" - - // EnvStorageBaseURL is the environment variable name for the ACME-DNS JSON account data. - // The URL to the storage server. - EnvStorageBaseURL = envNamespace + "STORAGE_BASE_URL" ) -var _ challenge.Provider = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIBase string - AllowList []string - StoragePath string - StorageBaseURL string -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{} -} - // acmeDNSClient is an interface describing the goacmedns.Client functions the DNSProvider uses. // It makes it easier for tests to shim a mock Client into the DNSProvider. type acmeDNSClient interface { // UpdateTXTRecord updates the provided account's TXT record // to the given value or returns an error. - UpdateTXTRecord(ctx context.Context, account goacmedns.Account, value string) error + UpdateTXTRecord(account goacmedns.Account, value string) error // RegisterAccount registers and returns a new account // with the given allowFrom restriction or returns an error. - RegisterAccount(ctx context.Context, allowFrom []string) (goacmedns.Account, error) + RegisterAccount(allowFrom []string) (goacmedns.Account, error) } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - config *Config client acmeDNSClient storage goacmedns.Storage } -// NewDNSProvider returns a DNSProvider instance configured for Joohoi's acme-dns. +// NewDNSProvider creates an ACME-DNS provider using file based account storage. +// Its configuration is loaded from the environment by reading EnvAPIBase and EnvStoragePath. func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIBase) + values, err := env.Get(EnvAPIBase, EnvStoragePath) if err != nil { return nil, fmt.Errorf("acme-dns: %w", err) } - config := NewDefaultConfig() - config.APIBase = values[EnvAPIBase] - config.StoragePath = env.GetOrFile(EnvStoragePath) - config.StorageBaseURL = env.GetOrFile(EnvStorageBaseURL) - - allowList := env.GetOrFile(EnvAllowList) - if allowList != "" { - config.AllowList = strings.Split(allowList, ",") - } - - return NewDNSProviderConfig(config) + client := goacmedns.NewClient(values[EnvAPIBase]) + storage := goacmedns.NewFileStorage(values[EnvStoragePath], 0o600) + return NewDNSProviderClient(client, storage) } -// NewDNSProviderConfig return a DNSProvider instance configured for Joohoi's acme-dns. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("acme-dns: the configuration of the DNS provider is nil") - } - - st, err := getStorage(config) - if err != nil { - return nil, fmt.Errorf("acme-dns: %w", err) - } - - client, err := goacmedns.NewClient(config.APIBase) - if err != nil { - return nil, fmt.Errorf("acme-dns: new client: %w", err) - } - - return &DNSProvider{ - config: config, - client: client, - storage: st, - }, nil -} - -// NewDNSProviderClient creates an ACME-DNS DNSProvider with the given acmeDNSClient and [goacmedns.Storage]. -// -// Deprecated: use [NewDNSProviderConfig] instead. -func NewDNSProviderClient(client acmeDNSClient, store goacmedns.Storage) (*DNSProvider, error) { +// NewDNSProviderClient creates an ACME-DNS DNSProvider with the given acmeDNSClient and goacmedns.Storage. +func NewDNSProviderClient(client acmeDNSClient, storage goacmedns.Storage) (*DNSProvider, error) { if client == nil { - return nil, errors.New("acme-dns: Client must be not nil") + return nil, errors.New("ACME-DNS Client must be not nil") } - if store == nil { - return nil, errors.New("acme-dns: Storage must be not nil") + if storage == nil { + return nil, errors.New("ACME-DNS Storage must be not nil") } return &DNSProvider{ - config: NewDefaultConfig(), client: client, - storage: store, + storage: storage, }, nil } @@ -165,28 +102,24 @@ func (e ErrCNAMERequired) Error() string { // one will be created and registered with the ACME DNS server and an ErrCNAMERequired error is returned. // This will halt issuance and indicate to the user that a one-time manual setup is required for the domain. func (d *DNSProvider) Present(domain, _, keyAuth string) error { - ctx := context.Background() - // Compute the challenge response FQDN and TXT value for the domain based on the keyAuth. info := dns01.GetChallengeInfo(domain, keyAuth) // Check if credentials were previously saved for this domain. - account, err := d.storage.Fetch(ctx, domain) + account, err := d.storage.Fetch(domain) if err != nil { - if !errors.Is(err, storage.ErrDomainNotFound) { - return err + if errors.Is(err, goacmedns.ErrDomainNotFound) { + // The account did not exist. + // Create a new one and return an error indicating the required one-time manual CNAME setup. + return d.register(domain, info.FQDN) } - // The account did not exist. - // Create a new one and return an error indicating the required one-time manual CNAME setup. - account, err = d.register(ctx, domain, info.FQDN) - if err != nil { - return err - } + // Errors other than goacmedns.ErrDomainNotFound are unexpected. + return err } // Update the acme-dns TXT record. - return d.client.UpdateTXTRecord(ctx, account, info.Value) + return d.client.UpdateTXTRecord(account, info.Value) } // CleanUp removes the record matching the specified parameters. It is not @@ -201,59 +134,29 @@ func (d *DNSProvider) CleanUp(_, _, _ string) error { // If account creation works as expected a ErrCNAMERequired error is returned describing // the one-time manual CNAME setup required to complete setup of the ACME-DNS hook for the domain. // If any other error occurs it is returned as-is. -func (d *DNSProvider) register(ctx context.Context, domain, fqdn string) (goacmedns.Account, error) { - newAcct, err := d.client.RegisterAccount(ctx, d.config.AllowList) +func (d *DNSProvider) register(domain, fqdn string) error { + // TODO(@cpu): Read CIDR whitelists from the environment + newAcct, err := d.client.RegisterAccount(nil) if err != nil { - return goacmedns.Account{}, err + return err } - var cnameCreated bool - // Store the new account in the storage and call save to persist the data. - err = d.storage.Put(ctx, domain, newAcct) + err = d.storage.Put(domain, newAcct) if err != nil { - cnameCreated = errors.Is(err, internal.ErrCNAMEAlreadyCreated) - if !cnameCreated { - return goacmedns.Account{}, err - } + return err } - - err = d.storage.Save(ctx) + err = d.storage.Save() if err != nil { - return goacmedns.Account{}, err - } - - if cnameCreated { - return newAcct, nil + return err } // Stop issuance by returning an error. // The user needs to perform a manual one-time CNAME setup in their DNS zone // to complete the setup of the new account we created. - return goacmedns.Account{}, ErrCNAMERequired{ + return ErrCNAMERequired{ Domain: domain, FQDN: fqdn, Target: newAcct.FullDomain, } } - -func getStorage(config *Config) (goacmedns.Storage, error) { - if config.StoragePath == "" && config.StorageBaseURL == "" { - return nil, errors.New("storagePath or storageBaseURL is not set") - } - - if config.StoragePath != "" && config.StorageBaseURL != "" { - return nil, errors.New("storagePath and storageBaseURL cannot be used at the same time") - } - - if config.StoragePath != "" { - return storage.NewFile(config.StoragePath, 0o600), nil - } - - st, err := internal.NewHTTPStorage(config.StorageBaseURL) - if err != nil { - return nil, fmt.Errorf("new HTTP storage: %w", err) - } - - return st, nil -} diff --git a/providers/dns/acmedns/acmedns.toml b/providers/dns/acmedns/acmedns.toml index e491569b0..4e17eb7ee 100644 --- a/providers/dns/acmedns/acmedns.toml +++ b/providers/dns/acmedns/acmedns.toml @@ -2,29 +2,19 @@ Name = "Joohoi's ACME-DNS" Description = '''''' URL = "https://github.com/joohoi/acme-dns" Code = "acme-dns" -Aliases = ["acmedns"] # TODO(ldez): remove "-" in v5 Since = "v1.1.0" Example = ''' ACME_DNS_API_BASE=http://10.0.0.8:4443 \ ACME_DNS_STORAGE_PATH=/root/.lego-acme-dns-accounts.json \ -lego --dns "acme-dns" -d '*.example.com' -d example.com run - -# or - -ACME_DNS_API_BASE=http://10.0.0.8:4443 \ -ACME_DNS_STORAGE_BASE_URL=http://10.10.10.10:80 \ -lego --dns "acme-dns" -d '*.example.com' -d example.com run +lego --email you@example.com --dns acme-dns --domains my.example.org run ''' [Configuration] [Configuration.Credentials] ACME_DNS_API_BASE = "The ACME-DNS API address" ACME_DNS_STORAGE_PATH = "The ACME-DNS JSON account data file. A per-domain account will be registered/persisted to this file and used for TXT updates." - ACME_DNS_STORAGE_BASE_URL = "The ACME-DNS JSON account data server." - [Configuration.Additional] - ACME_DNS_ALLOWLIST = "Source networks using CIDR notation (multiple values should be separated with a comma)." [Links] API = "https://github.com/joohoi/acme-dns#api" - GoClient = "https://github.com/nrdcg/goacmedns" + GoClient = "https://github.com/cpu/goacmedns" diff --git a/providers/dns/acmedns/acmedns_test.go b/providers/dns/acmedns/acmedns_test.go index a3ab59d59..bce36c7a6 100644 --- a/providers/dns/acmedns/acmedns_test.go +++ b/providers/dns/acmedns/acmedns_test.go @@ -1,28 +1,170 @@ package acmedns import ( - "net/http" - "net/http/httptest" + "errors" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/nrdcg/goacmedns" + "github.com/cpu/goacmedns" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +var ( + // errorClientErr is used by the Client mocks that return an error. + errorClientErr = errors.New("errorClient always errors") + // errorStorageErr is used by the Storage mocks that return an error. + errorStorageErr = errors.New("errorStorage always errors") +) + const ( + // Fixed test data for unit tests. egDomain = "example.com" egFQDN = "_acme-challenge." + egDomain + "." egKeyAuth = "⚷" ) +var egTestAccount = goacmedns.Account{ + FullDomain: "acme-dns." + egDomain, + SubDomain: "random-looking-junk." + egDomain, + Username: "spooky.mulder", + Password: "trustno1", +} + +// mockClient is a mock implementing the acmeDNSClient interface that always +// returns a fixed goacmedns.Account from calls to Register. +type mockClient struct { + mockAccount goacmedns.Account +} + +// UpdateTXTRecord does nothing. +func (c mockClient) UpdateTXTRecord(_ goacmedns.Account, _ string) error { + return nil +} + +// RegisterAccount returns c.mockAccount and no errors. +func (c mockClient) RegisterAccount(_ []string) (goacmedns.Account, error) { + return c.mockAccount, nil +} + +// mockUpdateClient is a mock implementing the acmeDNSClient interface that +// tracks the calls to UpdateTXTRecord in the records map. +type mockUpdateClient struct { + mockClient + records map[goacmedns.Account]string +} + +// UpdateTXTRecord saves a record value to c.records for the given acct. +func (c mockUpdateClient) UpdateTXTRecord(acct goacmedns.Account, value string) error { + c.records[acct] = value + return nil +} + +// errorRegisterClient is a mock implementing the acmeDNSClient interface that always +// returns errors from errorUpdateClient. +type errorUpdateClient struct { + mockClient +} + +// UpdateTXTRecord always returns an error. +func (c errorUpdateClient) UpdateTXTRecord(_ goacmedns.Account, _ string) error { + return errorClientErr +} + +// errorRegisterClient is a mock implementing the acmeDNSClient interface that always +// returns errors from RegisterAccount. +type errorRegisterClient struct { + mockClient +} + +// RegisterAccount always returns an error. +func (c errorRegisterClient) RegisterAccount(_ []string) (goacmedns.Account, error) { + return goacmedns.Account{}, errorClientErr +} + +// mockStorage is a mock implementing the goacmedns.Storage interface that +// returns static account data and ignores Save. +type mockStorage struct { + accounts map[string]goacmedns.Account +} + +// Save does nothing. +func (m mockStorage) Save() error { + return nil +} + +// Put stores an account for the given domain in m.accounts. +func (m mockStorage) Put(domain string, acct goacmedns.Account) error { + m.accounts[domain] = acct + return nil +} + +// Fetch retrieves an account for the given domain from m.accounts or returns +// goacmedns.ErrDomainNotFound. +func (m mockStorage) Fetch(domain string) (goacmedns.Account, error) { + if acct, ok := m.accounts[domain]; ok { + return acct, nil + } + return goacmedns.Account{}, goacmedns.ErrDomainNotFound +} + +// FetchAll returns all of m.accounts. +func (m mockStorage) FetchAll() map[string]goacmedns.Account { + return m.accounts +} + +// errorPutStorage is a mock implementing the goacmedns.Storage interface that +// always returns errors from Put. +type errorPutStorage struct { + mockStorage +} + +// Put always errors. +func (e errorPutStorage) Put(_ string, _ goacmedns.Account) error { + return errorStorageErr +} + +// errorSaveStorage is a mock implementing the goacmedns.Storage interface that +// always returns errors from Save. +type errorSaveStorage struct { + mockStorage +} + +// Save always errors. +func (e errorSaveStorage) Save() error { + return errorStorageErr +} + +// errorFetchStorage is a mock implementing the goacmedns.Storage interface that +// always returns errors from Fetch. +type errorFetchStorage struct { + mockStorage +} + +// Fetch always errors. +func (e errorFetchStorage) Fetch(_ string) (goacmedns.Account, error) { + return goacmedns.Account{}, errorStorageErr +} + +// FetchAll is a nop for errorFetchStorage. +func (e errorFetchStorage) FetchAll() map[string]goacmedns.Account { + return nil +} + +// TestPresent tests that the ACME-DNS Present function for updating a DNS-01 +// challenge response TXT record works as expected. func TestPresent(t *testing.T) { // validAccountStorage is a mockStorage configured to return the egTestAccount. - validAccountStorage := newMockStorage().WithAccount(egDomain, egTestAccount) - - // validUpdateClient is a mockClient configured with the egTestAccount that will track TXT updates in a map. - validUpdateClient := newMockClient() + validAccountStorage := mockStorage{ + map[string]goacmedns.Account{ + egDomain: egTestAccount, + }, + } + // validUpdateClient is a mockClient configured with the egTestAccount that will + // track TXT updates in a map. + validUpdateClient := mockUpdateClient{ + mockClient{egTestAccount}, + make(map[goacmedns.Account]string), + } testCases := []struct { Name string @@ -32,13 +174,13 @@ func TestPresent(t *testing.T) { }{ { Name: "present when client storage returns unexpected error", - Client: newMockClient().WithRegisterAccount(egTestAccount), - Storage: newMockStorage().WithFetchError(errorStorageErr), + Client: mockClient{egTestAccount}, + Storage: errorFetchStorage{}, ExpectedError: errorStorageErr, }, { Name: "present when client storage returns ErrDomainNotFound", - Client: newMockClient().WithRegisterAccount(egTestAccount), + Client: mockClient{egTestAccount}, ExpectedError: ErrCNAMERequired{ Domain: egDomain, FQDN: egFQDN, @@ -47,7 +189,7 @@ func TestPresent(t *testing.T) { }, { Name: "present when client UpdateTXTRecord returns unexpected error", - Client: newMockClient().WithUpdateTXTRecordError(errorClientErr), + Client: errorUpdateClient{}, Storage: validAccountStorage, ExpectedError: errorClientErr, }, @@ -60,17 +202,17 @@ func TestPresent(t *testing.T) { for _, test := range testCases { t.Run(test.Name, func(t *testing.T) { - p := &DNSProvider{ - config: NewDefaultConfig(), - client: test.Client, - storage: newMockStorage(), - } + dp, err := NewDNSProviderClient(test.Client, mockStorage{make(map[string]goacmedns.Account)}) + require.NoError(t, err) + // override the storage mock if required by the test case. if test.Storage != nil { - p.storage = test.Storage + dp.storage = test.Storage } - err := p.Present(egDomain, "foo", egKeyAuth) + // call Present. The token argument can be garbage because the ACME-DNS + // provider does not use it. + err = dp.Present(egDomain, "foo", egKeyAuth) if test.ExpectedError != nil { assert.Equal(t, test.ExpectedError, err) } else { @@ -86,33 +228,36 @@ func TestPresent(t *testing.T) { assert.Len(t, validUpdateClient.records[egTestAccount], 43) } +// TestRegister tests that the ACME-DNS register function works correctly. func TestRegister(t *testing.T) { testCases := []struct { Name string Client acmeDNSClient Storage goacmedns.Storage + Domain string + FQDN string ExpectedError error }{ { Name: "register when acme-dns client returns an error", - Client: newMockClient().WithRegisterAccountError(errorClientErr), + Client: errorRegisterClient{}, ExpectedError: errorClientErr, }, { Name: "register when acme-dns storage put returns an error", - Client: newMockClient().WithRegisterAccount(egTestAccount), - Storage: newMockStorage().WithPutError(errorStorageErr), + Client: mockClient{egTestAccount}, + Storage: errorPutStorage{mockStorage{make(map[string]goacmedns.Account)}}, ExpectedError: errorStorageErr, }, { Name: "register when acme-dns storage save returns an error", - Client: newMockClient().WithRegisterAccount(egTestAccount), - Storage: newMockStorage().WithSaveError(errorStorageErr), + Client: mockClient{egTestAccount}, + Storage: errorSaveStorage{mockStorage{make(map[string]goacmedns.Account)}}, ExpectedError: errorStorageErr, }, { Name: "register when everything works", - Client: newMockClient().WithRegisterAccount(egTestAccount), + Client: mockClient{egTestAccount}, ExpectedError: ErrCNAMERequired{ Domain: egDomain, FQDN: egFQDN, @@ -123,120 +268,20 @@ func TestRegister(t *testing.T) { for _, test := range testCases { t.Run(test.Name, func(t *testing.T) { - p := &DNSProvider{ - config: NewDefaultConfig(), - client: test.Client, - storage: newMockStorage(), - } + dp, err := NewDNSProviderClient(test.Client, mockStorage{make(map[string]goacmedns.Account)}) + require.NoError(t, err) + // override the storage mock if required by the testcase. if test.Storage != nil { - p.storage = test.Storage + dp.storage = test.Storage } - acc, err := p.register(t.Context(), egDomain, egFQDN) - if test.ExpectedError != nil { - assert.Equal(t, test.ExpectedError, err) - } else { - assert.Equal(t, goacmedns.Account{}, acc) - require.NoError(t, err) - } - }) - } -} - -func TestPresent_httpStorage(t *testing.T) { - testCases := []struct { - desc string - StatusCode int - ExpectedError error - }{ - { - desc: "the CNAME is not handled by the storage", - StatusCode: http.StatusOK, - ExpectedError: ErrCNAMERequired{ - Domain: egDomain, - FQDN: egFQDN, - Target: egTestAccount.FullDomain, - }, - }, - { - desc: "the CNAME is handled by the storage", - StatusCode: http.StatusCreated, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - provider := servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.StorageBaseURL = server.URL - - return NewDNSProviderConfig(config) - }). - // Fetch - Route("GET /example.com", servermock.Noop().WithStatusCode(http.StatusNotFound)). - // Put - Route("POST /example.com", servermock.Noop().WithStatusCode(test.StatusCode)). - Build(t) - - client := newMockClient().WithRegisterAccount(egTestAccount) - provider.client = client - - err := provider.Present(egDomain, "foo", egKeyAuth) - if test.ExpectedError != nil { - assert.EqualError(t, err, test.ExpectedError.Error()) - assert.True(t, client.registerAccountCalled) - assert.False(t, client.updateTXTRecordCalled) - } else { - require.NoError(t, err) - assert.True(t, client.registerAccountCalled) - assert.True(t, client.updateTXTRecordCalled) - } - }) - } -} - -func TestRegister_httpStorage(t *testing.T) { - testCases := []struct { - Name string - StatusCode int - ExpectedError error - }{ - { - Name: "status code 200", - StatusCode: http.StatusOK, - ExpectedError: ErrCNAMERequired{ - Domain: egDomain, - FQDN: egFQDN, - Target: egTestAccount.FullDomain, - }, - }, - { - Name: "status code 201", - StatusCode: http.StatusCreated, - }, - } - - for _, test := range testCases { - t.Run(test.Name, func(t *testing.T) { - provider := servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.StorageBaseURL = server.URL - - return NewDNSProviderConfig(config) - }). - // Put - Route("POST /example.com", servermock.Noop().WithStatusCode(test.StatusCode)). - Build(t) - - provider.client = newMockClient().WithRegisterAccount(egTestAccount) - - acc, err := provider.register(t.Context(), egDomain, egFQDN) + // Call register for the example domain/fqdn. + err = dp.register(egDomain, egFQDN) if test.ExpectedError != nil { assert.Equal(t, test.ExpectedError, err) } else { require.NoError(t, err) - assert.Equal(t, egTestAccount, acc) } }) } diff --git a/providers/dns/acmedns/internal/fixtures/error.json b/providers/dns/acmedns/internal/fixtures/error.json deleted file mode 100644 index d1b2ba3be..000000000 --- a/providers/dns/acmedns/internal/fixtures/error.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "message": "There is an error" -} diff --git a/providers/dns/acmedns/internal/fixtures/fetch-request.json b/providers/dns/acmedns/internal/fixtures/fetch-request.json deleted file mode 100644 index d29cebc5b..000000000 --- a/providers/dns/acmedns/internal/fixtures/fetch-request.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "fulldomain": "foo.example.com", - "subdomain": "foo", - "username": "user", - "password": "secret", - "server_url": "https://example.com" -} diff --git a/providers/dns/acmedns/internal/fixtures/fetch.json b/providers/dns/acmedns/internal/fixtures/fetch.json deleted file mode 100644 index d29cebc5b..000000000 --- a/providers/dns/acmedns/internal/fixtures/fetch.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "fulldomain": "foo.example.com", - "subdomain": "foo", - "username": "user", - "password": "secret", - "server_url": "https://example.com" -} diff --git a/providers/dns/acmedns/internal/fixtures/fetch_all.json b/providers/dns/acmedns/internal/fixtures/fetch_all.json deleted file mode 100644 index 9ea557b38..000000000 --- a/providers/dns/acmedns/internal/fixtures/fetch_all.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "a": { - "fulldomain": "foo.example.com", - "subdomain": "foo", - "username": "user", - "password": "secret", - "server_url": "https://example.com" - }, - "b": { - "fulldomain": "bar.example.com", - "subdomain": "bar", - "username": "user", - "password": "secret", - "server_url": "https://example.com" - } -} diff --git a/providers/dns/acmedns/internal/http_storage.go b/providers/dns/acmedns/internal/http_storage.go deleted file mode 100644 index 7a535eb20..000000000 --- a/providers/dns/acmedns/internal/http_storage.go +++ /dev/null @@ -1,147 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/nrdcg/goacmedns" - "github.com/nrdcg/goacmedns/storage" -) - -var _ goacmedns.Storage = (*HTTPStorage)(nil) - -var ErrCNAMEAlreadyCreated = errors.New("the CNAME has already been created") - -// HTTPStorage is an implementation of [acmedns.Storage] over HTTP. -type HTTPStorage struct { - client *http.Client - baseURL *url.URL -} - -// NewHTTPStorage created a new [HTTPStorage]. -func NewHTTPStorage(baseURL string) (*HTTPStorage, error) { - endpoint, err := url.Parse(baseURL) - if err != nil { - return nil, err - } - - return &HTTPStorage{ - client: &http.Client{Timeout: 2 * time.Minute}, - baseURL: endpoint, - }, nil -} - -func (s *HTTPStorage) Save(_ context.Context) error { - return nil -} - -func (s *HTTPStorage) Put(ctx context.Context, domain string, account goacmedns.Account) error { - req, err := newJSONRequest(ctx, http.MethodPost, s.baseURL.JoinPath(domain), account) - if err != nil { - return fmt.Errorf("unable to create request: %w", err) - } - - return s.do(req, nil) -} - -func (s *HTTPStorage) Fetch(ctx context.Context, domain string) (goacmedns.Account, error) { - req, err := newJSONRequest(ctx, http.MethodGet, s.baseURL.JoinPath(domain), nil) - if err != nil { - return goacmedns.Account{}, fmt.Errorf("unable to create request: %w", err) - } - - var account goacmedns.Account - - err = s.do(req, &account) - if err != nil { - return goacmedns.Account{}, err - } - - return account, nil -} - -func (s *HTTPStorage) FetchAll(ctx context.Context) (map[string]goacmedns.Account, error) { - req, err := newJSONRequest(ctx, http.MethodGet, s.baseURL, nil) - if err != nil { - return nil, err - } - - var mapping map[string]goacmedns.Account - - err = s.do(req, &mapping) - if err != nil { - return nil, err - } - - return mapping, nil -} - -func (s *HTTPStorage) do(req *http.Request, result any) error { - resp, err := s.client.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode == http.StatusNotFound { - return storage.ErrDomainNotFound - } - - if resp.StatusCode/100 != 2 { - return errutils.NewUnexpectedResponseStatusCodeError(req, resp) - } - - if result == nil { - // Hack related to `Put`. - if resp.StatusCode == http.StatusCreated { - return ErrCNAMEAlreadyCreated - } - - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} diff --git a/providers/dns/acmedns/internal/http_storage_test.go b/providers/dns/acmedns/internal/http_storage_test.go deleted file mode 100644 index 5c166b47f..000000000 --- a/providers/dns/acmedns/internal/http_storage_test.go +++ /dev/null @@ -1,153 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/nrdcg/goacmedns" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*HTTPStorage] { - return servermock.NewBuilder[*HTTPStorage]( - func(server *httptest.Server) (*HTTPStorage, error) { - storage, err := NewHTTPStorage(server.URL) - if err != nil { - return nil, err - } - - storage.client = server.Client() - - return storage, nil - }, - servermock.CheckHeader().WithJSONHeaders()) -} - -func TestHTTPStorage_Fetch(t *testing.T) { - storage := mockBuilder(). - Route("GET /example.com", servermock.ResponseFromFixture("fetch.json")). - Build(t) - - account, err := storage.Fetch(t.Context(), "example.com") - require.NoError(t, err) - - expected := goacmedns.Account{ - FullDomain: "foo.example.com", - SubDomain: "foo", - Username: "user", - Password: "secret", - ServerURL: "https://example.com", - } - - assert.Equal(t, expected, account) -} - -func TestHTTPStorage_Fetch_error(t *testing.T) { - storage := mockBuilder(). - Route("GET /example.com", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusInternalServerError)). - Build(t) - - _, err := storage.Fetch(t.Context(), "example.com") - require.Error(t, err) -} - -func TestHTTPStorage_FetchAll(t *testing.T) { - storage := mockBuilder(). - Route("GET /", servermock.ResponseFromFixture("fetch_all.json")). - Build(t) - - account, err := storage.FetchAll(t.Context()) - require.NoError(t, err) - - expected := map[string]goacmedns.Account{ - "a": { - FullDomain: "foo.example.com", - SubDomain: "foo", - Username: "user", - Password: "secret", - ServerURL: "https://example.com", - }, - "b": { - FullDomain: "bar.example.com", - SubDomain: "bar", - Username: "user", - Password: "secret", - ServerURL: "https://example.com", - }, - } - - assert.Equal(t, expected, account) -} - -func TestHTTPStorage_FetchAll_error(t *testing.T) { - storage := mockBuilder(). - Route("GET /", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusInternalServerError)). - Build(t) - - _, err := storage.FetchAll(t.Context()) - require.Error(t, err) -} - -func TestHTTPStorage_Put(t *testing.T) { - storage := mockBuilder(). - Route("POST /example.com", nil, - servermock.CheckRequestJSONBodyFromFixture("fetch-request.json")). - Build(t) - - account := goacmedns.Account{ - FullDomain: "foo.example.com", - SubDomain: "foo", - Username: "user", - Password: "secret", - ServerURL: "https://example.com", - } - - err := storage.Put(t.Context(), "example.com", account) - require.NoError(t, err) -} - -func TestHTTPStorage_Put_error(t *testing.T) { - storage := mockBuilder(). - Route("POST /example.com", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusInternalServerError)). - Build(t) - - account := goacmedns.Account{ - FullDomain: "foo.example.com", - SubDomain: "foo", - Username: "user", - Password: "secret", - ServerURL: "https://example.com", - } - - err := storage.Put(t.Context(), "example.com", account) - require.Error(t, err) -} - -func TestHTTPStorage_Put_CNAME_created(t *testing.T) { - storage := mockBuilder(). - Route("POST /example.com", - servermock.Noop(). - WithStatusCode(http.StatusCreated), - servermock.CheckRequestJSONBodyFromFixture("fetch-request.json")). - Build(t) - - account := goacmedns.Account{ - FullDomain: "foo.example.com", - SubDomain: "foo", - Username: "user", - Password: "secret", - ServerURL: "https://example.com", - } - - err := storage.Put(t.Context(), "example.com", account) - require.ErrorIs(t, err, ErrCNAMEAlreadyCreated) -} diff --git a/providers/dns/acmedns/internal/readme.md b/providers/dns/acmedns/internal/readme.md deleted file mode 100644 index b667d3d23..000000000 --- a/providers/dns/acmedns/internal/readme.md +++ /dev/null @@ -1,72 +0,0 @@ -# HTTP Storage - -## Fetch - -### Request - -Endpoint: `GET /` - -### Response - -Response status code 200. - -Response body (account): - -```json -{ - "fulldomain": "foo.example.com", - "subdomain": "foo", - "username": "user", - "password": "secret", - "server_url": "https://example.com" -} -``` - -## Fetch All - -### Request - -Endpoint: `GET ` - -### Response - -Response status code 200. - -Response body (domain/account mapping): - -```json -{ - "foo.example.com": { - "fulldomain": "foo.example.com", - "subdomain": "foo", - "username": "user", - "password": "secret", - "server_url": "https://example.com" - }, - "bar.example.com": { - "fulldomain": "bar.example.com", - "subdomain": "bar", - "username": "user", - "password": "secret", - "server_url": "https://example.com" - } -} -``` - -## Put - -### Request - -Endpoint: `POST /` - -### Response - -Response status code: -- 200: the process will be stopped to allow the user to create the CNAME. -- 201: the process will continue without error (the CNAME should be created by the server) - -No expected body. - -## Save - -No dedicated endpoint. diff --git a/providers/dns/acmedns/mock_test.go b/providers/dns/acmedns/mock_test.go deleted file mode 100644 index a09a3ca91..000000000 --- a/providers/dns/acmedns/mock_test.go +++ /dev/null @@ -1,161 +0,0 @@ -package acmedns - -import ( - "context" - "errors" - - "github.com/nrdcg/goacmedns" - "github.com/nrdcg/goacmedns/storage" -) - -var ( - // errorClientErr is used by the Client mocks that return an error. - errorClientErr = errors.New("errorClient always errors") - // errorStorageErr is used by the Storage mocks that return an error. - errorStorageErr = errors.New("errorStorage always errors") -) - -var egTestAccount = goacmedns.Account{ - FullDomain: "acme-dns." + egDomain, - SubDomain: "random-looking-junk." + egDomain, - Username: "spooky.mulder", - Password: "trustno1", -} - -type mockClient struct { - records map[goacmedns.Account]string - - updateTXTRecordCalled bool - updateTXTRecord func(ctx context.Context, acct goacmedns.Account, value string) error - - registerAccountCalled bool - registerAccount func(ctx context.Context, allowFrom []string) (goacmedns.Account, error) -} - -func newMockClient() *mockClient { - return &mockClient{ - records: make(map[goacmedns.Account]string), - updateTXTRecord: func(_ context.Context, _ goacmedns.Account, _ string) error { - return nil - }, - registerAccount: func(_ context.Context, _ []string) (goacmedns.Account, error) { - return goacmedns.Account{}, nil - }, - } -} - -func (c *mockClient) UpdateTXTRecord(ctx context.Context, acct goacmedns.Account, value string) error { - c.updateTXTRecordCalled = true - c.records[acct] = value - - return c.updateTXTRecord(ctx, acct, value) -} - -func (c *mockClient) RegisterAccount(ctx context.Context, allowFrom []string) (goacmedns.Account, error) { - c.registerAccountCalled = true - return c.registerAccount(ctx, allowFrom) -} - -func (c *mockClient) WithUpdateTXTRecordError(err error) *mockClient { - c.updateTXTRecord = func(_ context.Context, _ goacmedns.Account, _ string) error { - return err - } - - return c -} - -func (c *mockClient) WithRegisterAccount(acct goacmedns.Account) *mockClient { - c.registerAccount = func(_ context.Context, _ []string) (goacmedns.Account, error) { - return acct, nil - } - - return c -} - -func (c *mockClient) WithRegisterAccountError(err error) *mockClient { - c.registerAccount = func(_ context.Context, _ []string) (goacmedns.Account, error) { - return goacmedns.Account{}, err - } - - return c -} - -type mockStorage struct { - accounts map[string]goacmedns.Account - fetchAll func(ctx context.Context) (map[string]goacmedns.Account, error) - fetch func(ctx context.Context, domain string) (goacmedns.Account, error) - put func(ctx context.Context, domain string, acct goacmedns.Account) error - save func(ctx context.Context) error -} - -func newMockStorage() *mockStorage { - m := &mockStorage{ - accounts: make(map[string]goacmedns.Account), - put: func(_ context.Context, _ string, _ goacmedns.Account) error { - return nil - }, - save: func(_ context.Context) error { - return nil - }, - } - - m.fetchAll = func(ctx context.Context) (map[string]goacmedns.Account, error) { - return m.accounts, nil - } - - m.fetch = func(_ context.Context, domain string) (goacmedns.Account, error) { - if acct, ok := m.accounts[domain]; ok { - return acct, nil - } - - return goacmedns.Account{}, storage.ErrDomainNotFound - } - - return m -} - -func (m *mockStorage) FetchAll(ctx context.Context) (map[string]goacmedns.Account, error) { - return m.fetchAll(ctx) -} - -func (m *mockStorage) Fetch(ctx context.Context, domain string) (goacmedns.Account, error) { - return m.fetch(ctx, domain) -} - -func (m *mockStorage) Put(ctx context.Context, domain string, account goacmedns.Account) error { - return m.put(ctx, domain, account) -} - -func (m *mockStorage) Save(ctx context.Context) error { - return m.save(ctx) -} - -func (m *mockStorage) WithAccount(domain string, acct goacmedns.Account) *mockStorage { - m.accounts[domain] = acct - - return m -} - -func (m *mockStorage) WithFetchError(err error) *mockStorage { - m.fetch = func(_ context.Context, _ string) (goacmedns.Account, error) { - return goacmedns.Account{}, err - } - - return m -} - -func (m *mockStorage) WithPutError(err error) *mockStorage { - m.put = func(_ context.Context, _ string, _ goacmedns.Account) error { - return err - } - - return m -} - -func (m *mockStorage) WithSaveError(err error) *mockStorage { - m.save = func(ctx context.Context) error { - return err - } - - return m -} diff --git a/providers/dns/active24/active24.go b/providers/dns/active24/active24.go deleted file mode 100644 index 0b925de6a..000000000 --- a/providers/dns/active24/active24.go +++ /dev/null @@ -1,103 +0,0 @@ -// Package active24 implements a DNS provider for solving the DNS-01 challenge using Active24. -package active24 - -import ( - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/active24" -) - -const baseAPIDomain = "active24.cz" - -// Environment variables names. -const ( - envNamespace = "ACTIVE24_" - - EnvAPIKey = envNamespace + "API_KEY" - EnvSecret = envNamespace + "SECRET" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config = active24.Config - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - prv challenge.ProviderTimeout -} - -// NewDNSProvider returns a DNSProvider instance configured for Active24. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIKey, EnvSecret) - if err != nil { - return nil, fmt.Errorf("active24: %w", err) - } - - config := NewDefaultConfig() - config.APIKey = values[EnvAPIKey] - config.Secret = values[EnvSecret] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Active24. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("active24: the configuration of the DNS provider is nil") - } - - provider, err := active24.NewDNSProviderConfig(config, baseAPIDomain) - if err != nil { - return nil, fmt.Errorf("active24: %w", err) - } - - return &DNSProvider{prv: provider}, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - err := d.prv.Present(domain, token, keyAuth) - if err != nil { - return fmt.Errorf("active24: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - err := d.prv.CleanUp(domain, token, keyAuth) - if err != nil { - return fmt.Errorf("active24: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.prv.Timeout() -} diff --git a/providers/dns/active24/active24.toml b/providers/dns/active24/active24.toml deleted file mode 100644 index b0eaabab8..000000000 --- a/providers/dns/active24/active24.toml +++ /dev/null @@ -1,25 +0,0 @@ -Name = "Active24" -Description = '''''' -URL = "https://www.active24.cz" -Code = "active24" -Since = "v4.23.0" - -Example = ''' -ACTIVE24_API_KEY="xxx" \ -ACTIVE24_SECRET="yyy" \ -lego --dns active24 -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - ACTIVE24_API_KEY = "API key" - ACTIVE24_SECRET = "Secret" - [Configuration.Additional] - ACTIVE24_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - ACTIVE24_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - ACTIVE24_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - ACTIVE24_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://rest.active24.cz/v2/docs" - APIv1 = "https://rest.active24.cz/docs/v1.service#services" diff --git a/providers/dns/active24/active24_test.go b/providers/dns/active24/active24_test.go deleted file mode 100644 index 2987fb27b..000000000 --- a/providers/dns/active24/active24_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package active24 - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAPIKey, EnvSecret).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAPIKey: "user", - EnvSecret: "secret", - }, - }, - { - desc: "missing API key", - envVars: map[string]string{ - EnvAPIKey: "", - EnvSecret: "secret", - }, - expected: "active24: some credentials information are missing: ACTIVE24_API_KEY", - }, - { - desc: "missing secret", - envVars: map[string]string{ - EnvAPIKey: "user", - EnvSecret: "", - }, - expected: "active24: some credentials information are missing: ACTIVE24_SECRET", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "active24: some credentials information are missing: ACTIVE24_API_KEY,ACTIVE24_SECRET", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.prv) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiKey string - secret string - expected string - }{ - { - desc: "success", - apiKey: "user", - secret: "secret", - }, - { - desc: "missing API key", - apiKey: "", - secret: "secret", - expected: "active24: credentials missing", - }, - { - desc: "missing secret", - apiKey: "user", - secret: "", - expected: "active24: credentials missing", - }, - { - desc: "missing credentials", - expected: "active24: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIKey = test.apiKey - config.Secret = test.secret - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.prv) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/alidns/alidns.go b/providers/dns/alidns/alidns.go index cdd8e75e0..803567e1d 100644 --- a/providers/dns/alidns/alidns.go +++ b/providers/dns/alidns/alidns.go @@ -2,22 +2,22 @@ package alidns import ( - "context" "errors" "fmt" "time" - openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" - "github.com/alibabacloud-go/tea/dara" - "github.com/aliyun/credentials-go/credentials" - alidns "github.com/go-acme/alidns-20150109/v4/client" - "github.com/go-acme/lego/v4/challenge" + "github.com/aliyun/alibaba-cloud-sdk-go/sdk" + "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth" + "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials" + "github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests" + "github.com/aliyun/alibaba-cloud-sdk-go/services/alidns" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/ptr" "golang.org/x/net/idna" ) +const defaultRegionID = "cn-hangzhou" + // Environment variables names. const ( envNamespace = "ALICLOUD_" @@ -27,7 +27,6 @@ const ( EnvSecretKey = envNamespace + "SECRET_KEY" EnvSecurityToken = envNamespace + "SECURITY_TOKEN" EnvRegionID = envNamespace + "REGION_ID" - EnvLine = envNamespace + "LINE" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" @@ -35,10 +34,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const defaultRegionID = "cn-hangzhou" - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { RAMRole string @@ -46,7 +41,6 @@ type Config struct { SecretKey string SecurityToken string RegionID string - Line string PropagationTimeout time.Duration PollingInterval time.Duration TTL int @@ -76,7 +70,6 @@ type DNSProvider struct { func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() config.RegionID = env.GetOrFile(EnvRegionID) - config.Line = env.GetOrFile(EnvLine) values, err := env.Get(EnvRAMRole) if err == nil { @@ -106,42 +99,23 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { config.RegionID = defaultRegionID } - cfg := new(openapi.Config). - SetRegionId(config.RegionID). - SetReadTimeout(int(config.HTTPTimeout.Milliseconds())) - + var credential auth.Credential switch { case config.RAMRole != "": - // https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance - credentialsCfg := new(credentials.Config). - SetType("ecs_ram_role"). - SetRoleName(config.RAMRole) - - credentialClient, err := credentials.NewCredential(credentialsCfg) - if err != nil { - return nil, fmt.Errorf("alicloud: new credential: %w", err) - } - - cfg = cfg.SetCredential(credentialClient) - + credential = credentials.NewEcsRamRoleCredential(config.RAMRole) case config.APIKey != "" && config.SecretKey != "" && config.SecurityToken != "": - cfg = cfg. - SetAccessKeyId(config.APIKey). - SetAccessKeySecret(config.SecretKey). - SetSecurityToken(config.SecurityToken) - + credential = credentials.NewStsTokenCredential(config.APIKey, config.SecretKey, config.SecurityToken) case config.APIKey != "" && config.SecretKey != "": - cfg = cfg. - SetAccessKeyId(config.APIKey). - SetAccessKeySecret(config.SecretKey) - + credential = credentials.NewAccessKeyCredential(config.APIKey, config.SecretKey) default: return nil, errors.New("alicloud: ram role or credentials missing") } - client, err := alidns.NewClient(cfg) + conf := sdk.NewConfig().WithTimeout(config.HTTPTimeout) + + client, err := alidns.NewClientWithOptions(config.RegionID, conf, credential) if err != nil { - return nil, fmt.Errorf("alicloud: new client: %w", err) + return nil, fmt.Errorf("alicloud: credentials failed: %w", err) } return &DNSProvider{config: config, client: client}, nil @@ -155,76 +129,67 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) - zoneName, err := d.getHostedZone(ctx, info.EffectiveFQDN) + zoneName, err := d.getHostedZone(info.EffectiveFQDN) if err != nil { return fmt.Errorf("alicloud: %w", err) } - recordRequest, err := d.newTxtRecord(zoneName, info.EffectiveFQDN, info.Value) + recordAttributes, err := d.newTxtRecord(zoneName, info.EffectiveFQDN, info.Value) if err != nil { return err } - _, err = alidns.AddDomainRecordWithContext(ctx, d.client, recordRequest, &dara.RuntimeOptions{}) + _, err = d.client.AddDomainRecord(recordAttributes) if err != nil { return fmt.Errorf("alicloud: API call failed: %w", err) } - return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) - records, err := d.findTxtRecords(ctx, info.EffectiveFQDN) + records, err := d.findTxtRecords(info.EffectiveFQDN) if err != nil { return fmt.Errorf("alicloud: %w", err) } - _, err = d.getHostedZone(ctx, info.EffectiveFQDN) + _, err = d.getHostedZone(info.EffectiveFQDN) if err != nil { return fmt.Errorf("alicloud: %w", err) } for _, rec := range records { - request := &alidns.DeleteDomainRecordRequest{ - RecordId: rec.RecordId, - } - - _, err = alidns.DeleteDomainRecordWithContext(ctx, d.client, request, &dara.RuntimeOptions{}) + request := alidns.CreateDeleteDomainRecordRequest() + request.RecordId = rec.RecordId + _, err = d.client.DeleteDomainRecord(request) if err != nil { return fmt.Errorf("alicloud: %w", err) } } - return nil } -func (d *DNSProvider) getHostedZone(ctx context.Context, domain string) (string, error) { - request := new(alidns.DescribeDomainsRequest) +func (d *DNSProvider) getHostedZone(domain string) (string, error) { + request := alidns.CreateDescribeDomainsRequest() - var domains []*alidns.DescribeDomainsResponseBodyDomainsDomain - - var startPage int64 = 1 + var domains []alidns.DomainInDescribeDomains + startPage := 1 for { - request.SetPageNumber(startPage) + request.PageNumber = requests.NewInteger(startPage) - response, err := alidns.DescribeDomainsWithContext(ctx, d.client, request, &dara.RuntimeOptions{}) + response, err := d.client.DescribeDomains(request) if err != nil { return "", fmt.Errorf("API call failed: %w", err) } - domains = append(domains, response.Body.Domains.Domain...) + domains = append(domains, response.Domains.Domain...) - if ptr.Deref(response.Body.PageNumber)*ptr.Deref(response.Body.PageSize) >= ptr.Deref(response.Body.TotalCount) { + if response.PageNumber*response.PageSize >= response.TotalCount { break } @@ -236,54 +201,50 @@ func (d *DNSProvider) getHostedZone(ctx context.Context, domain string) (string, return "", fmt.Errorf("could not find zone: %w", err) } - var hostedZone *alidns.DescribeDomainsResponseBodyDomainsDomain - + var hostedZone alidns.DomainInDescribeDomains for _, zone := range domains { - if ptr.Deref(zone.DomainName) == dns01.UnFqdn(authZone) || ptr.Deref(zone.PunyCode) == dns01.UnFqdn(authZone) { + if zone.DomainName == dns01.UnFqdn(authZone) || zone.PunyCode == dns01.UnFqdn(authZone) { hostedZone = zone } } - if hostedZone == nil || ptr.Deref(hostedZone.DomainId) == "" { + if hostedZone.DomainId == "" { return "", fmt.Errorf("zone %s not found in AliDNS for domain %s", authZone, domain) } - return ptr.Deref(hostedZone.DomainName), nil + return hostedZone.DomainName, nil } func (d *DNSProvider) newTxtRecord(zone, fqdn, value string) (*alidns.AddDomainRecordRequest, error) { - rr, err := extractRecordName(fqdn, zone) + request := alidns.CreateAddDomainRecordRequest() + request.Type = "TXT" + request.DomainName = zone + + var err error + request.RR, err = extractRecordName(fqdn, zone) if err != nil { return nil, err } - adrr := new(alidns.AddDomainRecordRequest). - SetType("TXT"). - SetDomainName(zone). - SetRR(rr). - SetValue(value). - SetTTL(int64(d.config.TTL)) + request.Value = value + request.TTL = requests.NewInteger(d.config.TTL) - if d.config.Line != "" { - adrr.SetLine(d.config.Line) - } - - return adrr, nil + return request, nil } -func (d *DNSProvider) findTxtRecords(ctx context.Context, fqdn string) ([]*alidns.DescribeDomainRecordsResponseBodyDomainRecordsRecord, error) { - zoneName, err := d.getHostedZone(ctx, fqdn) +func (d *DNSProvider) findTxtRecords(fqdn string) ([]alidns.Record, error) { + zoneName, err := d.getHostedZone(fqdn) if err != nil { return nil, err } - request := new(alidns.DescribeDomainRecordsRequest). - SetDomainName(zoneName). - SetPageSize(500) + request := alidns.CreateDescribeDomainRecordsRequest() + request.DomainName = zoneName + request.PageSize = requests.NewInteger(500) - var records []*alidns.DescribeDomainRecordsResponseBodyDomainRecordsRecord + var records []alidns.Record - result, err := alidns.DescribeDomainRecordsWithContext(ctx, d.client, request, &dara.RuntimeOptions{}) + result, err := d.client.DescribeDomainRecords(request) if err != nil { return records, fmt.Errorf("API call has failed: %w", err) } @@ -293,12 +254,11 @@ func (d *DNSProvider) findTxtRecords(ctx context.Context, fqdn string) ([]*alidn return nil, err } - for _, record := range result.Body.DomainRecords.Record { - if ptr.Deref(record.RR) == recordName && ptr.Deref(record.Type) == "TXT" { + for _, record := range result.DomainRecords.Record { + if record.RR == recordName && record.Type == "TXT" { records = append(records, record) } } - return records, nil } diff --git a/providers/dns/alidns/alidns.toml b/providers/dns/alidns/alidns.toml index b78e1859d..875307443 100644 --- a/providers/dns/alidns/alidns.toml +++ b/providers/dns/alidns/alidns.toml @@ -7,30 +7,27 @@ Since = "v1.1.0" Example = ''' # Setup using instance RAM role ALICLOUD_RAM_ROLE=lego \ -lego --dns alidns -d '*.example.com' -d example.com run +lego --email you@example.com --dns alidns --domains my.example.org run # Or, using credentials ALICLOUD_ACCESS_KEY=abcdefghijklmnopqrstuvwx \ ALICLOUD_SECRET_KEY=your-secret-key \ ALICLOUD_SECURITY_TOKEN=your-sts-token \ -lego --dns alidns - -d '*.example.com' -d example.com run +lego --email you@example.com --dns alidns --domains my.example.org run ''' [Configuration] [Configuration.Credentials] - ALICLOUD_RAM_ROLE = "Your instance RAM role (https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance)" + ALICLOUD_RAM_ROLE = "Your instance RAM role (https://www.alibabacloud.com/help/doc-detail/54579.htm)" ALICLOUD_ACCESS_KEY = "Access key ID" ALICLOUD_SECRET_KEY = "Access Key secret" ALICLOUD_SECURITY_TOKEN = "STS Security Token (optional)" [Configuration.Additional] - ALICLOUD_REGION_ID = "Region ID (Default: cn-hangzhou)" - ALICLOUD_LINE = "Line (Default: default)" - ALICLOUD_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - ALICLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - ALICLOUD_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)" - ALICLOUD_HTTP_TIMEOUT = "API request timeout in seconds (Default: 10)" + ALICLOUD_POLLING_INTERVAL = "Time between DNS propagation check" + ALICLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + ALICLOUD_TTL = "The TTL of the TXT record used for the DNS challenge" + ALICLOUD_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.alibabacloud.com/help/en/alibaba-cloud-dns/latest/api-alidns-2015-01-09-dir-parsing-records" - GoClient = "https://github.com/alibabacloud-go/alidns-20150109" - GoClient2 = "https://github.com/aliyun/alibabacloud-go-sdk/tree/HEAD/alidns-20150109" + GoClient = "https://github.com/aliyun/alibaba-cloud-sdk-go" diff --git a/providers/dns/alidns/alidns_test.go b/providers/dns/alidns/alidns_test.go index b1e482d2d..487997813 100644 --- a/providers/dns/alidns/alidns_test.go +++ b/providers/dns/alidns/alidns_test.go @@ -64,7 +64,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -143,7 +142,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -157,7 +155,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/aliesa/aliesa.go b/providers/dns/aliesa/aliesa.go deleted file mode 100644 index 2a38389be..000000000 --- a/providers/dns/aliesa/aliesa.go +++ /dev/null @@ -1,255 +0,0 @@ -// Package aliesa implements a DNS provider for solving the DNS-01 challenge using AlibabaCloud ESA. -package aliesa - -import ( - "context" - "errors" - "fmt" - "sync" - "time" - - openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" - "github.com/alibabacloud-go/tea/dara" - "github.com/aliyun/credentials-go/credentials" - esa "github.com/go-acme/esa-20240910/v2/client" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/ptr" -) - -// Environment variables names. -const ( - envNamespace = "ALIESA_" - - EnvRAMRole = envNamespace + "RAM_ROLE" - EnvAccessKey = envNamespace + "ACCESS_KEY" - EnvSecretKey = envNamespace + "SECRET_KEY" - EnvSecurityToken = envNamespace + "SECURITY_TOKEN" - EnvRegionID = envNamespace + "REGION_ID" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -const defaultRegionID = "cn-hangzhou" - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - RAMRole string - APIKey string - SecretKey string - SecurityToken string - RegionID string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPTimeout time.Duration -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *esa.Client - - recordIDs map[string]int64 - recordIDsMu sync.Mutex -} - -// NewDNSProvider returns a DNSProvider instance configured for AlibabaCloud ESA. -func NewDNSProvider() (*DNSProvider, error) { - config := NewDefaultConfig() - config.RegionID = env.GetOrFile(EnvRegionID) - - values, err := env.Get(EnvRAMRole) - if err == nil { - config.RAMRole = values[EnvRAMRole] - return NewDNSProviderConfig(config) - } - - values, err = env.Get(EnvAccessKey, EnvSecretKey) - if err != nil { - return nil, fmt.Errorf("aliesa: %w", err) - } - - config.APIKey = values[EnvAccessKey] - config.SecretKey = values[EnvSecretKey] - config.SecurityToken = env.GetOrFile(EnvSecurityToken) - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for AlibabaCloud ESA. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("aliesa: the configuration of the DNS provider is nil") - } - - if config.RegionID == "" { - config.RegionID = defaultRegionID - } - - cfg := new(openapi.Config). - SetRegionId(config.RegionID). - SetReadTimeout(int(config.HTTPTimeout.Milliseconds())) - - switch { - case config.RAMRole != "": - // https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance - credentialsCfg := new(credentials.Config). - SetType("ecs_ram_role"). - SetRoleName(config.RAMRole) - - credentialClient, err := credentials.NewCredential(credentialsCfg) - if err != nil { - return nil, fmt.Errorf("aliesa: new credential: %w", err) - } - - cfg = cfg.SetCredential(credentialClient) - - case config.APIKey != "" && config.SecretKey != "" && config.SecurityToken != "": - cfg = cfg. - SetAccessKeyId(config.APIKey). - SetAccessKeySecret(config.SecretKey). - SetSecurityToken(config.SecurityToken) - - case config.APIKey != "" && config.SecretKey != "": - cfg = cfg. - SetAccessKeyId(config.APIKey). - SetAccessKeySecret(config.SecretKey) - - default: - return nil, errors.New("aliesa: ram role or credentials missing") - } - - client, err := esa.NewClient(cfg) - if err != nil { - return nil, fmt.Errorf("aliesa: new client: %w", err) - } - - // Workaround to get a regional URL. - // https://github.com/alibabacloud-go/esa-20240910/blame/7660e3aab2045d4820e4b83427a154efe0c79319/client/client.go#L27 - // The `EndpointRule` is hardcoded with an empty string, so the region is ignored. - client.Endpoint = nil - client.EndpointRule = ptr.Pointer("regional") - - client.Endpoint, err = esa.GetEndpoint(client, dara.String("esa"), client.RegionId, client.EndpointRule, client.Network, client.Suffix, client.EndpointMap, client.Endpoint) - if err != nil { - return nil, fmt.Errorf("aliesa: get endpoint: %w", err) - } - - return &DNSProvider{ - config: config, - client: client, - recordIDs: make(map[string]int64), - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - siteID, err := d.getSiteID(ctx, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("aliesa: %w", err) - } - - crReq := new(esa.CreateRecordRequest). - SetSiteId(siteID). - SetType("TXT"). - SetRecordName(dns01.UnFqdn(info.EffectiveFQDN)). - SetTtl(int32(d.config.TTL)). - SetData(new(esa.CreateRecordRequestData).SetValue(info.Value)) - - // https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-createrecord - crResp, err := esa.CreateRecordWithContext(ctx, d.client, crReq, &dara.RuntimeOptions{}) - if err != nil { - return fmt.Errorf("aliesa: create record: %w", err) - } - - d.recordIDsMu.Lock() - d.recordIDs[token] = ptr.Deref(crResp.Body.GetRecordId()) - d.recordIDsMu.Unlock() - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - // gets the record's unique ID - d.recordIDsMu.Lock() - recordID, ok := d.recordIDs[token] - d.recordIDsMu.Unlock() - - if !ok { - return fmt.Errorf("aliesa: unknown record ID for '%s'", info.EffectiveFQDN) - } - - drReq := new(esa.DeleteRecordRequest). - SetRecordId(recordID) - - // https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-deleterecord - _, err := esa.DeleteRecordWithContext(ctx, d.client, drReq, &dara.RuntimeOptions{}) - if err != nil { - return fmt.Errorf("aliesa: delete record: %w", err) - } - - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -func (d *DNSProvider) getSiteID(ctx context.Context, fqdn string) (int64, error) { - authZone, err := dns01.FindZoneByFqdn(fqdn) - if err != nil { - return 0, fmt.Errorf("aliesa: could not find zone for domain %q: %w", fqdn, err) - } - - lsReq := new(esa.ListSitesRequest). - SetSiteName(dns01.UnFqdn(authZone)). - SetSiteSearchType("suffix") - - // https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-listsites - lsResp, err := esa.ListSitesWithContext(ctx, d.client, lsReq, &dara.RuntimeOptions{}) - if err != nil { - return 0, fmt.Errorf("list sites: %w", err) - } - - for f := range dns01.UnFqdnDomainsSeq(fqdn) { - domain := dns01.UnFqdn(f) - - for _, site := range lsResp.Body.GetSites() { - if ptr.Deref(site.GetSiteName()) == domain { - return ptr.Deref(site.GetSiteId()), nil - } - } - } - - return 0, fmt.Errorf("site not found (fqdn: %q)", fqdn) -} diff --git a/providers/dns/aliesa/aliesa.toml b/providers/dns/aliesa/aliesa.toml deleted file mode 100644 index 5e7345e40..000000000 --- a/providers/dns/aliesa/aliesa.toml +++ /dev/null @@ -1,33 +0,0 @@ -Name = "AlibabaCloud ESA" -Description = '''''' -URL = "https://www.alibabacloud.com/en/product/esa" -Code = "aliesa" -Since = "v4.29.0" - -Example = ''' -# Setup using instance RAM role -ALIESA_RAM_ROLE=lego \ -lego --dns aliesa -d '*.example.com' -d example.com run - -# Or, using credentials -ALIESA_ACCESS_KEY=abcdefghijklmnopqrstuvwx \ -ALIESA_SECRET_KEY=your-secret-key \ -ALIESA_SECURITY_TOKEN=your-sts-token \ -lego --dns aliesa - -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - ALIESA_RAM_ROLE = "Your instance RAM role (https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance)" - ALIESA_ACCESS_KEY = "Access key ID" - ALIESA_SECRET_KEY = "Access Key secret" - ALIESA_SECURITY_TOKEN = "STS Security Token (optional)" - [Configuration.Additional] - ALIESA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - ALIESA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - ALIESA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - ALIESA_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-overview?spm=a2c63.p38356.help-menu-2673927.d_6_0_0.20b224c28PSZDc#:~:text=DNS-,DNS%20records,-DNS%20records" - GoClient = "https://github.com/alibabacloud-go/esa-20240910" diff --git a/providers/dns/allinkl/allinkl.go b/providers/dns/allinkl/allinkl.go index 376b0903c..aaaca844c 100644 --- a/providers/dns/allinkl/allinkl.go +++ b/providers/dns/allinkl/allinkl.go @@ -9,12 +9,9 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/allinkl/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -29,8 +26,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Login string @@ -94,16 +89,12 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { identifier.HTTPClient = config.HTTPClient } - identifier.HTTPClient = clientdebug.Wrap(identifier.HTTPClient) - client := internal.NewClient(config.Login) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, identifier: identifier, @@ -122,20 +113,20 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("allinkl: could not find zone for domain %q: %w", domain, err) + } + ctx := context.Background() credential, err := d.identifier.Authentication(ctx, 60, true) if err != nil { - return fmt.Errorf("allinkl: authentication: %w", err) + return fmt.Errorf("allinkl: %w", err) } ctx = internal.WithContext(ctx, credential) - authZone, err := d.findZone(ctx, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("allinkl: %w", err) - } - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) if err != nil { return fmt.Errorf("allinkl: %w", err) @@ -150,7 +141,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { recordID, err := d.client.AddDNSSettings(ctx, record) if err != nil { - return fmt.Errorf("allinkl: add DNS settings: %w", err) + return fmt.Errorf("allinkl: %w", err) } d.recordIDsMu.Lock() @@ -168,7 +159,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { credential, err := d.identifier.Authentication(ctx, 60, true) if err != nil { - return fmt.Errorf("allinkl: authentication: %w", err) + return fmt.Errorf("allinkl: %w", err) } ctx = internal.WithContext(ctx, credential) @@ -177,33 +168,14 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() - if !ok { return fmt.Errorf("allinkl: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) } _, err = d.client.DeleteDNSSettings(ctx, recordID) if err != nil { - return fmt.Errorf("allinkl: delete DNS settings: %w", err) + return fmt.Errorf("allinkl: %w", err) } - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - return nil } - -func (d *DNSProvider) findZone(ctx context.Context, fqdn string) (string, error) { - for z := range dns01.DomainsSeq(fqdn) { - _, errG := d.client.GetDNSSettings(ctx, z, "") - if errG != nil { - log.Infof("get DNS settings zone[%q] %v", z, errG) - continue - } - - return z, nil - } - - return "", fmt.Errorf("unable to find auth zone for '%s'", fqdn) -} diff --git a/providers/dns/allinkl/allinkl.toml b/providers/dns/allinkl/allinkl.toml index 774f8fb9f..29534f34c 100644 --- a/providers/dns/allinkl/allinkl.toml +++ b/providers/dns/allinkl/allinkl.toml @@ -7,7 +7,7 @@ Since = "v4.5.0" Example = ''' ALL_INKL_LOGIN=xxxxxxxxxxxxxxxxxxxxxxxxxx \ ALL_INKL_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \ -lego --dns allinkl -d '*.example.com' -d example.com run +lego --email you@example.com --dns allinkl --domains my.example.org run ''' [Configuration] @@ -15,9 +15,9 @@ lego --dns allinkl -d '*.example.com' -d example.com run ALL_INKL_LOGIN = "KAS login" ALL_INKL_PASSWORD = "KAS password" [Configuration.Additional] - ALL_INKL_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - ALL_INKL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - ALL_INKL_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + ALL_INKL_POLLING_INTERVAL = "Time between DNS propagation check" + ALL_INKL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + ALL_INKL_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://kasapi.kasserver.com/dokumentation/phpdoc/index.html" diff --git a/providers/dns/allinkl/allinkl_test.go b/providers/dns/allinkl/allinkl_test.go index 7da47aee4..af85f8c54 100644 --- a/providers/dns/allinkl/allinkl_test.go +++ b/providers/dns/allinkl/allinkl_test.go @@ -1,18 +1,9 @@ package allinkl import ( - "encoding/json" - "encoding/xml" - "fmt" - "io" - "net/http" - "net/http/httptest" - "net/url" "testing" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/go-acme/lego/v4/providers/dns/allinkl/internal" "github.com/stretchr/testify/require" ) @@ -62,7 +53,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -131,7 +121,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -145,115 +134,9 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.Login = "user" - config.Password = "secret" - config.HTTPClient = server.Client() - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - p.client.BaseURL, _ = url.Parse(server.URL) - p.identifier.BaseURL, _ = url.Parse(server.URL) - - return p, err - }, - ).Route("POST /KasAuth.php", - servermock.ResponseFromInternal("auth.xml"), - servermock.CheckRequestBodyFromInternal("auth-request.xml"). - IgnoreWhitespace(), - ) -} - -func extractKasRequest(reader io.Reader) (*internal.KasRequest, error) { - type ReqEnvelope struct { - XMLName xml.Name `xml:"Envelope"` - Body struct { - KasAPI struct { - Params string `xml:"Params"` - } `xml:"KasApi"` - } `xml:"Body"` - } - - raw, err := io.ReadAll(reader) - if err != nil { - return nil, err - } - - reqEnvelope := ReqEnvelope{} - - err = xml.Unmarshal(raw, &reqEnvelope) - if err != nil { - return nil, err - } - - var kReq internal.KasRequest - - err = json.Unmarshal([]byte(reqEnvelope.Body.KasAPI.Params), &kReq) - if err != nil { - return nil, err - } - - return &kReq, nil -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("POST /KasApi.php", - http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - kReq, err := extractKasRequest(req.Body) - if err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) - return - } - - switch kReq.Action { - case "get_dns_settings": - params := kReq.RequestParams.(map[string]any) - - if params["zone_host"] == "_acme-challenge.example.com." { - servermock.ResponseFromInternal("get_dns_settings_not_found.xml").ServeHTTP(rw, req) - } else { - servermock.ResponseFromInternal("get_dns_settings.xml").ServeHTTP(rw, req) - } - - case "add_dns_settings": - servermock.ResponseFromInternal("add_dns_settings.xml").ServeHTTP(rw, req) - - default: - http.Error(rw, fmt.Sprintf("unknown action: %v", kReq.Action), http.StatusBadRequest) - } - }), - ). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("POST /KasApi.php", - servermock.ResponseFromInternal("delete_dns_settings.xml"), - servermock.CheckRequestBodyFromInternal("delete_dns_settings-request.xml"). - IgnoreWhitespace()). - Build(t) - - provider.recordIDs["abc"] = "57347450" - - err := provider.CleanUp("example.com", "abc", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/allinkl/internal/client.go b/providers/dns/allinkl/internal/client.go index d4403cac5..ab8cf9a38 100644 --- a/providers/dns/allinkl/internal/client.go +++ b/providers/dns/allinkl/internal/client.go @@ -6,21 +6,16 @@ import ( "encoding/json" "fmt" "net/http" - "net/url" "strconv" "strings" "sync" "time" - "github.com/cenkalti/backoff/v5" - "github.com/go-acme/lego/v4/platform/wait" "github.com/go-acme/lego/v4/providers/dns/internal/errutils" "github.com/go-viper/mapstructure/v2" ) -const defaultBaseURL = "https://kasapi.kasserver.com/soap/" - -const apiPath = "KasApi.php" +const apiEndpoint = "https://kasapi.kasserver.com/soap/KasApi.php" type Authentication interface { Authentication(ctx context.Context, sessionLifetime int, sessionUpdateLifetime bool) (string, error) @@ -33,21 +28,16 @@ type Client struct { floodTime time.Time muFloodTime sync.Mutex - maxElapsedTime time.Duration - - BaseURL *url.URL + baseURL string HTTPClient *http.Client } // NewClient creates a new Client. func NewClient(login string) *Client { - baseURL, _ := url.Parse(defaultBaseURL) - return &Client{ - login: login, - BaseURL: baseURL, - maxElapsedTime: 3 * time.Minute, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, + login: login, + baseURL: apiEndpoint, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, } } @@ -61,9 +51,13 @@ func (c *Client) GetDNSSettings(ctx context.Context, zone, recordID string) ([]R requestParams["record_id"] = recordID } - var g APIResponse[GetDNSSettingsResponse] + req, err := c.newRequest(ctx, "get_dns_settings", requestParams) + if err != nil { + return nil, err + } - err := c.doRequest(ctx, "get_dns_settings", requestParams, &g) + var g GetDNSSettingsAPIResponse + err = c.do(req, &g) if err != nil { return nil, err } @@ -75,9 +69,13 @@ func (c *Client) GetDNSSettings(ctx context.Context, zone, recordID string) ([]R // AddDNSSettings Creation of a DNS resource record. func (c *Client) AddDNSSettings(ctx context.Context, record DNSRequest) (string, error) { - var g APIResponse[AddDNSSettingsResponse] + req, err := c.newRequest(ctx, "add_dns_settings", record) + if err != nil { + return "", err + } - err := c.doRequest(ctx, "add_dns_settings", record, &g) + var g AddDNSSettingsAPIResponse + err = c.do(req, &g) if err != nil { return "", err } @@ -88,19 +86,23 @@ func (c *Client) AddDNSSettings(ctx context.Context, record DNSRequest) (string, } // DeleteDNSSettings Deleting a DNS Resource Record. -func (c *Client) DeleteDNSSettings(ctx context.Context, recordID string) (string, error) { +func (c *Client) DeleteDNSSettings(ctx context.Context, recordID string) (bool, error) { requestParams := map[string]string{"record_id": recordID} - var g APIResponse[DeleteDNSSettingsResponse] - - err := c.doRequest(ctx, "delete_dns_settings", requestParams, &g) + req, err := c.newRequest(ctx, "delete_dns_settings", requestParams) if err != nil { - return "", err + return false, err + } + + var g DeleteDNSSettingsAPIResponse + err = c.do(req, &g) + if err != nil { + return false, err } c.updateFloodTime(g.Response.KasFloodDelay) - return g.Response.ReturnString, nil + return g.Response.ReturnInfo, nil } func (c *Client) newRequest(ctx context.Context, action string, requestParams any) (*http.Request, error) { @@ -119,9 +121,7 @@ func (c *Client) newRequest(ctx context.Context, action string, requestParams an payload := []byte(strings.TrimSpace(fmt.Sprintf(kasAPIEnvelope, body))) - endpoint := c.BaseURL.JoinPath(apiPath) - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), bytes.NewReader(payload)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL, bytes.NewReader(payload)) if err != nil { return nil, fmt.Errorf("unable to create request: %w", err) } @@ -129,21 +129,6 @@ func (c *Client) newRequest(ctx context.Context, action string, requestParams an return req, nil } -func (c *Client) doRequest(ctx context.Context, action string, requestParams, result any) error { - return wait.Retry(ctx, - func() error { - req, err := c.newRequest(ctx, action, requestParams) - if err != nil { - return backoff.Permanent(err) - } - - return c.do(req, result) - }, - backoff.WithBackOff(&backoff.ZeroBackOff{}), - backoff.WithMaxElapsedTime(c.maxElapsedTime), - ) -} - func (c *Client) do(req *http.Request, result any) error { c.muFloodTime.Lock() time.Sleep(time.Until(c.floodTime)) @@ -151,40 +136,29 @@ func (c *Client) do(req *http.Request, result any) error { resp, err := c.HTTPClient.Do(req) if err != nil { - return backoff.Permanent(errutils.NewHTTPDoError(req, err)) + return errutils.NewHTTPDoError(req, err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - return backoff.Permanent(errutils.NewUnexpectedResponseStatusCodeError(req, resp)) + return errutils.NewUnexpectedResponseStatusCodeError(req, resp) } envlp, err := decodeXML[KasAPIResponseEnvelope](resp.Body) if err != nil { - return backoff.Permanent(err) + return err } if envlp.Body.Fault != nil { - if envlp.Body.Fault.Message == "flood_protection" { - ft, errP := strconv.ParseFloat(envlp.Body.Fault.Detail, 64) - if errP != nil { - return fmt.Errorf("parse flood protection delay: %w", envlp.Body.Fault) - } - - c.updateFloodTime(ft) - - return envlp.Body.Fault - } - - return backoff.Permanent(envlp.Body.Fault) + return envlp.Body.Fault } raw := getValue(envlp.Body.KasAPIResponse.Return) err = mapstructure.Decode(raw, result) if err != nil { - return backoff.Permanent(fmt.Errorf("response struct decode: %w", err)) + return fmt.Errorf("response struct decode: %w", err) } return nil diff --git a/providers/dns/allinkl/internal/client_test.go b/providers/dns/allinkl/internal/client_test.go index 949f45bf9..3eb7c21a9 100644 --- a/providers/dns/allinkl/internal/client_test.go +++ b/providers/dns/allinkl/internal/client_test.go @@ -1,34 +1,29 @@ package internal import ( + "fmt" + "io" + "net/http" "net/http/httptest" - "net/url" + "os" + "path/filepath" "testing" - "time" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func setupClient(server *httptest.Server) (*Client, error) { - client := NewClient("user") - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - client.maxElapsedTime = 1 * time.Second - - return client, nil -} - func TestClient_GetDNSSettings(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("POST /KasApi.php", servermock.ResponseFromFixture("get_dns_settings.xml"), - servermock.CheckRequestBodyFromFixture("get_dns_settings-request.xml"). - IgnoreWhitespace()). - Build(t) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) - records, err := client.GetDNSSettings(mockContext(t), "example.com", "") + mux.HandleFunc("/", testHandler("get_dns_settings.xml")) + + client := NewClient("user") + client.baseURL = server.URL + + records, err := client.GetDNSSettings(mockContext(), "example.com", "") require.NoError(t, err) expected := []ReturnInfo{ @@ -100,27 +95,15 @@ func TestClient_GetDNSSettings(t *testing.T) { assert.Equal(t, expected, records) } -func TestClient_GetDNSSettings_error_flood_protection(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("POST /KasApi.php", - servermock.ResponseFromFixture("flood_protection.xml"), - ). - Build(t) - - assert.Zero(t, client.floodTime) - - _, err := client.GetDNSSettings(mockContext(t), "example.com", "") - require.EqualError(t, err, "KasApi: SOAP-ENV:Server: flood_protection: 0.0688529014587") - - assert.NotZero(t, client.floodTime) -} - func TestClient_AddDNSSettings(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("POST /KasApi.php", servermock.ResponseFromFixture("add_dns_settings.xml"), - servermock.CheckRequestBodyFromFixture("add_dns_settings-request.xml"). - IgnoreWhitespace()). - Build(t) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc("/", testHandler("add_dns_settings.xml")) + + client := NewClient("user") + client.baseURL = server.URL record := DNSRequest{ ZoneHost: "42cnc.de.", @@ -129,21 +112,47 @@ func TestClient_AddDNSSettings(t *testing.T) { RecordData: "abcdefgh", } - recordID, err := client.AddDNSSettings(mockContext(t), record) + recordID, err := client.AddDNSSettings(mockContext(), record) require.NoError(t, err) assert.Equal(t, "57347444", recordID) } func TestClient_DeleteDNSSettings(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("POST /KasApi.php", servermock.ResponseFromFixture("delete_dns_settings.xml"), - servermock.CheckRequestBodyFromFixture("delete_dns_settings-request.xml"). - IgnoreWhitespace()). - Build(t) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) - r, err := client.DeleteDNSSettings(mockContext(t), "57347450") + mux.HandleFunc("/", testHandler("delete_dns_settings.xml")) + + client := NewClient("user") + client.baseURL = server.URL + + r, err := client.DeleteDNSSettings(mockContext(), "57347450") require.NoError(t, err) - assert.Equal(t, "TRUE", r) + assert.True(t, r) +} + +func testHandler(filename string) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + } } diff --git a/providers/dns/allinkl/internal/fixtures/add_dns_settings-request.xml b/providers/dns/allinkl/internal/fixtures/add_dns_settings-request.xml deleted file mode 100644 index e8cd12633..000000000 --- a/providers/dns/allinkl/internal/fixtures/add_dns_settings-request.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - {"kas_login":"user","kas_auth_type":"session","kas_auth_data":"593959ca04f0de9689b586c6a647d15d","kas_action":"add_dns_settings","KasRequestParams":{"zone_host":"42cnc.de.","record_type":"TXT","record_name":"lego","record_data":"abcdefgh","record_aux":0}} - - - diff --git a/providers/dns/allinkl/internal/fixtures/auth-request.xml b/providers/dns/allinkl/internal/fixtures/auth-request.xml deleted file mode 100644 index 1cba86f10..000000000 --- a/providers/dns/allinkl/internal/fixtures/auth-request.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - {"kas_login":"user","kas_auth_data":"secret","kas_auth_type":"plain","session_lifetime":60,"session_update_lifetime":"Y"} - - - diff --git a/providers/dns/allinkl/internal/fixtures/delete_dns_settings-request.xml b/providers/dns/allinkl/internal/fixtures/delete_dns_settings-request.xml deleted file mode 100644 index a306a98a7..000000000 --- a/providers/dns/allinkl/internal/fixtures/delete_dns_settings-request.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - {"kas_login":"user","kas_auth_type":"session","kas_auth_data":"593959ca04f0de9689b586c6a647d15d","kas_action":"delete_dns_settings","KasRequestParams":{"record_id":"57347450"}} - - - diff --git a/providers/dns/allinkl/internal/fixtures/flood_protection.xml b/providers/dns/allinkl/internal/fixtures/flood_protection.xml deleted file mode 100644 index b8da10fab..000000000 --- a/providers/dns/allinkl/internal/fixtures/flood_protection.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - SOAP-ENV:Server - flood_protection - KasApi - 0.0688529014587 - - - diff --git a/providers/dns/allinkl/internal/fixtures/get_dns_settings-request.xml b/providers/dns/allinkl/internal/fixtures/get_dns_settings-request.xml deleted file mode 100644 index b44941d2b..000000000 --- a/providers/dns/allinkl/internal/fixtures/get_dns_settings-request.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - {"kas_login":"user","kas_auth_type":"session","kas_auth_data":"593959ca04f0de9689b586c6a647d15d","kas_action":"get_dns_settings","KasRequestParams":{"zone_host":"example.com"}} - - - diff --git a/providers/dns/allinkl/internal/fixtures/get_dns_settings-zone_not_found.xml b/providers/dns/allinkl/internal/fixtures/get_dns_settings-zone_not_found.xml deleted file mode 100644 index 478d07a3a..000000000 --- a/providers/dns/allinkl/internal/fixtures/get_dns_settings-zone_not_found.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - SOAP-ENV:Server - zone_not_found - KasApi - example.com - - - diff --git a/providers/dns/allinkl/internal/fixtures/get_dns_settings-zone_syntax_incorrect.xml b/providers/dns/allinkl/internal/fixtures/get_dns_settings-zone_syntax_incorrect.xml deleted file mode 100644 index c77d733db..000000000 --- a/providers/dns/allinkl/internal/fixtures/get_dns_settings-zone_syntax_incorrect.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - SOAP-ENV:Server - zone_syntax_incorrect - KasApi - _acme-challenge.example.com - - - diff --git a/providers/dns/allinkl/internal/identity.go b/providers/dns/allinkl/internal/identity.go index e95e78899..4353ece31 100644 --- a/providers/dns/allinkl/internal/identity.go +++ b/providers/dns/allinkl/internal/identity.go @@ -6,14 +6,14 @@ import ( "encoding/json" "fmt" "net/http" - "net/url" "strings" "time" "github.com/go-acme/lego/v4/providers/dns/internal/errutils" ) -const authPath = "KasAuth.php" +// authEndpoint represents the Identity API endpoint to call. +const authEndpoint = "https://kasapi.kasserver.com/soap/KasAuth.php" type token string @@ -24,19 +24,17 @@ type Identifier struct { login string password string - BaseURL *url.URL - HTTPClient *http.Client + authEndpoint string + HTTPClient *http.Client } // NewIdentifier creates a new Identifier. -func NewIdentifier(login, password string) *Identifier { - baseURL, _ := url.Parse(defaultBaseURL) - +func NewIdentifier(login string, password string) *Identifier { return &Identifier{ - login: login, - password: password, - BaseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, + login: login, + password: password, + authEndpoint: authEndpoint, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, } } @@ -64,9 +62,7 @@ func (c *Identifier) Authentication(ctx context.Context, sessionLifetime int, se payload := []byte(strings.TrimSpace(fmt.Sprintf(kasAuthEnvelope, body))) - endpoint := c.BaseURL.JoinPath(authPath) - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), bytes.NewReader(payload)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.authEndpoint, bytes.NewReader(payload)) if err != nil { return "", fmt.Errorf("unable to create request: %w", err) } diff --git a/providers/dns/allinkl/internal/identity_test.go b/providers/dns/allinkl/internal/identity_test.go index 41d092b13..0753f3862 100644 --- a/providers/dns/allinkl/internal/identity_test.go +++ b/providers/dns/allinkl/internal/identity_test.go @@ -2,48 +2,44 @@ package internal import ( "context" + "net/http" "net/http/httptest" - "net/url" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func setupIdentifierClient(server *httptest.Server) (*Identifier, error) { - client := NewIdentifier("user", "secret") - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil -} - -func mockContext(t *testing.T) context.Context { - t.Helper() - - return context.WithValue(t.Context(), tokenKey, "593959ca04f0de9689b586c6a647d15d") +func mockContext() context.Context { + return context.WithValue(context.Background(), tokenKey, "593959ca04f0de9689b586c6a647d15d") } func TestIdentifier_Authentication(t *testing.T) { - client := servermock.NewBuilder[*Identifier](setupIdentifierClient). - Route("POST /KasAuth.php", - servermock.ResponseFromFixture("auth.xml"), - servermock.CheckRequestBodyFromFixture("auth-request.xml"). - IgnoreWhitespace()). - Build(t) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) - credentialToken, err := client.Authentication(t.Context(), 60, true) + mux.HandleFunc("/", testHandler("auth.xml")) + + client := NewIdentifier("user", "secret") + client.authEndpoint = server.URL + + credentialToken, err := client.Authentication(context.Background(), 60, false) require.NoError(t, err) assert.Equal(t, "593959ca04f0de9689b586c6a647d15d", credentialToken) } func TestIdentifier_Authentication_error(t *testing.T) { - client := servermock.NewBuilder[*Identifier](setupIdentifierClient). - Route("POST /KasAuth.php", servermock.ResponseFromFixture("auth_fault.xml")). - Build(t) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) - _, err := client.Authentication(t.Context(), 60, false) + mux.HandleFunc("/", testHandler("auth_fault.xml")) + + client := NewIdentifier("user", "secret") + client.authEndpoint = server.URL + + _, err := client.Authentication(context.Background(), 60, false) require.Error(t, err) } diff --git a/providers/dns/allinkl/internal/types.go b/providers/dns/allinkl/internal/types.go index 51f7065b5..b5c6ba0d1 100644 --- a/providers/dns/allinkl/internal/types.go +++ b/providers/dns/allinkl/internal/types.go @@ -17,7 +17,6 @@ func (tr Trimmer) Token() (xml.Token, error) { if cd, ok := t.(xml.CharData); ok { t = xml.CharData(bytes.TrimSpace(cd)) } - return t, err } @@ -26,11 +25,10 @@ type Fault struct { Code string `xml:"faultcode"` Message string `xml:"faultstring"` Actor string `xml:"faultactor"` - Detail string `xml:"detail"` } -func (f *Fault) Error() string { - return fmt.Sprintf("%s: %s: %s: %s", f.Actor, f.Code, f.Message, f.Detail) +func (f Fault) Error() string { + return fmt.Sprintf("%s: %s: %s", f.Actor, f.Code, f.Message) } // KasResponse a KAS SOAP response. @@ -55,7 +53,6 @@ func decodeXML[T any](reader io.Reader) (*T, error) { } var result T - err = xml.NewTokenDecoder(Trimmer{decoder: xml.NewDecoder(bytes.NewReader(raw))}).Decode(&result) if err != nil { return nil, fmt.Errorf("decode XML response: %w", err) diff --git a/providers/dns/allinkl/internal/types_api.go b/providers/dns/allinkl/internal/types_api.go index a11f3aac0..145163cda 100644 --- a/providers/dns/allinkl/internal/types_api.go +++ b/providers/dns/allinkl/internal/types_api.go @@ -53,8 +53,8 @@ type DNSRequest struct { // --- -type APIResponse[T any] struct { - Response T `json:"Response" mapstructure:"Response"` +type GetDNSSettingsAPIResponse struct { + Response GetDNSSettingsResponse `json:"Response" mapstructure:"Response"` } type GetDNSSettingsResponse struct { @@ -73,14 +73,22 @@ type ReturnInfo struct { Aux int `json:"record_aux,omitempty" mapstructure:"record_aux"` } +type AddDNSSettingsAPIResponse struct { + Response AddDNSSettingsResponse `json:"Response" mapstructure:"Response"` +} + type AddDNSSettingsResponse struct { KasFloodDelay float64 `json:"KasFloodDelay" mapstructure:"KasFloodDelay"` ReturnInfo string `json:"ReturnInfo" mapstructure:"ReturnInfo"` ReturnString string `json:"ReturnString" mapstructure:"ReturnString"` } +type DeleteDNSSettingsAPIResponse struct { + Response DeleteDNSSettingsResponse `json:"Response"` +} + type DeleteDNSSettingsResponse struct { KasFloodDelay float64 `json:"KasFloodDelay"` + ReturnInfo bool `json:"ReturnInfo"` ReturnString string `json:"ReturnString"` - // NOTE: ReturnInfo (!= ReturnString) doesn't seem to have a stable type } diff --git a/providers/dns/alwaysdata/alwaysdata.go b/providers/dns/alwaysdata/alwaysdata.go deleted file mode 100644 index b2e0f3957..000000000 --- a/providers/dns/alwaysdata/alwaysdata.go +++ /dev/null @@ -1,185 +0,0 @@ -// Package alwaysdata implements a DNS provider for solving the DNS-01 challenge using Alwaysdata. -package alwaysdata - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/alwaysdata/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "ALWAYSDATA_" - - EnvAPIKey = envNamespace + "API_KEY" - EnvAccount = envNamespace + "ACCOUNT" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - Account string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for Alwaysdata. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIKey) - if err != nil { - return nil, fmt.Errorf("alwaysdata: %w", err) - } - - config := NewDefaultConfig() - config.APIKey = values[EnvAPIKey] - config.Account = env.GetOrFile(EnvAccount) - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Alwaysdata. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("alwaysdata: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.APIKey, config.Account) - if err != nil { - return nil, fmt.Errorf("alwaysdata: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - zone, err := d.findZone(ctx, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("alwaysdata: %w", err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name) - if err != nil { - return fmt.Errorf("alwaysdata: %w", err) - } - - record := internal.RecordRequest{ - DomainID: zone.ID, - Name: subDomain, - Type: "TXT", - Value: info.Value, - TTL: d.config.TTL, - Annotation: "lego", - } - - err = d.client.AddRecord(ctx, record) - if err != nil { - return fmt.Errorf("alwaysdata: add TXT record: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - zone, err := d.findZone(ctx, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("alwaysdata: %w", err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name) - if err != nil { - return fmt.Errorf("alwaysdata: %w", err) - } - - records, err := d.client.ListRecords(ctx, zone.ID, subDomain) - if err != nil { - return fmt.Errorf("alwaysdata: list records: %w", err) - } - - for _, record := range records { - if record.Type != "TXT" || record.Value != info.Value { - continue - } - - err = d.client.DeleteRecord(ctx, record.ID) - if err != nil { - return fmt.Errorf("alwaysdata: delete TXT record: %w", err) - } - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -func (d *DNSProvider) findZone(ctx context.Context, fqdn string) (*internal.Domain, error) { - domains, err := d.client.ListDomains(ctx) - if err != nil { - return nil, fmt.Errorf("list domains: %w", err) - } - - for a := range dns01.UnFqdnDomainsSeq(fqdn) { - for _, domain := range domains { - if a == domain.Name { - return &domain, nil - } - } - } - - return nil, errors.New("domain not found") -} diff --git a/providers/dns/alwaysdata/alwaysdata.toml b/providers/dns/alwaysdata/alwaysdata.toml deleted file mode 100644 index d00c6f032..000000000 --- a/providers/dns/alwaysdata/alwaysdata.toml +++ /dev/null @@ -1,26 +0,0 @@ -Name = "Alwaysdata" -Description = '''''' -URL = "https://alwaysdata.com/" -Code = "alwaysdata" -Since = "v4.31.0" - -Example = ''' -ALWAYSDATA_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns alwaysdata -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - ALWAYSDATA_API_KEY = "API Key" - [Configuration.Additional] - ALWAYSDATA_ACCOUNT = "Account name" - ALWAYSDATA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - ALWAYSDATA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - ALWAYSDATA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - ALWAYSDATA_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://help.alwaysdata.com/en/api/resources/" - APIDocDomains = "https://api.alwaysdata.com/v1/domain/doc/" - APIDocRecords = "https://api.alwaysdata.com/v1/record/doc/" - APIExamples = "https://help.alwaysdata.com/en/api/examples/" diff --git a/providers/dns/alwaysdata/alwaysdata_test.go b/providers/dns/alwaysdata/alwaysdata_test.go deleted file mode 100644 index 6084c2ae4..000000000 --- a/providers/dns/alwaysdata/alwaysdata_test.go +++ /dev/null @@ -1,187 +0,0 @@ -package alwaysdata - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAPIKey, EnvAccount).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAPIKey: "secret", - }, - }, - { - desc: "success with an account", - envVars: map[string]string{ - EnvAPIKey: "secret", - EnvAccount: "foo", - }, - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "alwaysdata: some credentials information are missing: ALWAYSDATA_API_KEY", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiKey string - account string - expected string - }{ - { - desc: "success", - apiKey: "secret", - }, - { - desc: "success with an account", - apiKey: "secret", - account: "foo", - }, - { - desc: "missing credentials", - expected: "alwaysdata: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIKey = test.apiKey - config.Account = test.account - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIKey = "secret" - config.HTTPClient = server.Client() - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - p.client.BaseURL, _ = url.Parse(server.URL) - - return p, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(). - WithBasicAuth("secret", ""), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("GET /domain/", - servermock.ResponseFromInternal("domains.json")). - Route("POST /record/", - servermock.Noop().WithStatusCode(http.StatusCreated), - servermock.CheckRequestJSONBodyFromInternal("record_add-request.json")). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("GET /domain/", - servermock.ResponseFromInternal("domains.json")). - Route("GET /record/", - servermock.ResponseFromInternal("records.json"), - servermock.CheckQueryParameter().Strict(). - With("domain", "132"). - With("name", "_acme-challenge"), - ). - Route("DELETE /record/789/", - servermock.Noop()). - Build(t) - - err := provider.CleanUp("example.com", "abc", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/alwaysdata/internal/client.go b/providers/dns/alwaysdata/internal/client.go deleted file mode 100644 index 5db11dcd1..000000000 --- a/providers/dns/alwaysdata/internal/client.go +++ /dev/null @@ -1,177 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" -) - -const defaultBaseURL = "https://api.alwaysdata.com/v1" - -// Client the Alwaysdata API client. -type Client struct { - apiKey string - account string - - BaseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(apiKey, account string) (*Client, error) { - if apiKey == "" { - return nil, errors.New("credentials missing") - } - - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - apiKey: apiKey, - account: account, - BaseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) { - endpoint := c.BaseURL.JoinPath("domain", "/") - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var result []Domain - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return result, nil -} - -func (c *Client) AddRecord(ctx context.Context, record RecordRequest) error { - endpoint := c.BaseURL.JoinPath("record", "/") - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) - if err != nil { - return err - } - - err = c.do(req, nil) - if err != nil { - return err - } - - return nil -} - -func (c *Client) DeleteRecord(ctx context.Context, recordID int64) error { - endpoint := c.BaseURL.JoinPath("record", strconv.FormatInt(recordID, 10), "/") - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return err - } - - return c.do(req, nil) -} - -func (c *Client) ListRecords(ctx context.Context, domainID int64, name string) ([]Record, error) { - endpoint := c.BaseURL.JoinPath("record", "/") - - query := endpoint.Query() - query.Set("domain", strconv.FormatInt(domainID, 10)) - query.Set("name", name) - endpoint.RawQuery = query.Encode() - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var result []Record - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return result, nil -} - -func (c *Client) do(req *http.Request, result any) error { - useragent.SetHeader(req.Header) - - user := c.apiKey - - if c.account != "" { - user += "account=" + c.account - } - - req.SetBasicAuth(user, "") - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - raw, _ := io.ReadAll(resp.Body) - - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} diff --git a/providers/dns/alwaysdata/internal/client_test.go b/providers/dns/alwaysdata/internal/client_test.go deleted file mode 100644 index e6a349662..000000000 --- a/providers/dns/alwaysdata/internal/client_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("secret", "") - if err != nil { - return nil, err - } - - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = clientdebug.Wrap(server.Client()) - - return client, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(). - WithBasicAuth("secret", ""), - ) -} - -func TestClient_ListDomains(t *testing.T) { - client := mockBuilder(). - Route("GET /domain/", - servermock.ResponseFromFixture("domains.json")). - Build(t) - - result, err := client.ListDomains(t.Context()) - require.NoError(t, err) - - expected := []Domain{ - {ID: 132, Name: "example.com", Annotation: "test"}, - {ID: 133, Name: "example.net", IsInternal: true}, - {ID: 134, Name: "example.org"}, - } - - assert.Equal(t, expected, result) -} - -func TestClient_AddRecord(t *testing.T) { - t.Setenv("LEGO_DEBUG_DNS_API_HTTP_CLIENT", "true") - - client := mockBuilder(). - Route("POST /record/", - servermock.Noop().WithStatusCode(http.StatusCreated), - servermock.CheckRequestJSONBodyFromFixture("record_add-request.json")). - Build(t) - - record := RecordRequest{ - DomainID: 132, - Name: "_acme-challenge", - Type: "TXT", - Value: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: 120, - Annotation: "lego", - } - - err := client.AddRecord(t.Context(), record) - require.NoError(t, err) -} - -func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /record/789/", - servermock.Noop()). - Build(t) - - err := client.DeleteRecord(t.Context(), 789) - require.NoError(t, err) -} - -func TestClient_ListRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /record/", - servermock.ResponseFromFixture("records.json"), - servermock.CheckQueryParameter().Strict(). - With("domain", "132"). - With("name", "_acme-challenge"), - ). - Build(t) - - result, err := client.ListRecords(t.Context(), 132, "_acme-challenge") - require.NoError(t, err) - - expected := []Record{ - { - ID: 789, - Domain: &Domain{ - Href: "/v1/domain/132/", - }, - Type: "TXT", - Name: "_acme-challenge", - Value: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: 120, - Annotation: "lego", - }, - { - ID: 11619270, - Domain: &Domain{ - Href: "/v1/domain/118935/", - }, - Name: "home", - Type: "A", - Value: "149.202.90.65", - TTL: 300, - IsUserDefined: true, - IsActive: true, - }, - } - - assert.Equal(t, expected, result) -} diff --git a/providers/dns/alwaysdata/internal/fixtures/domains.json b/providers/dns/alwaysdata/internal/fixtures/domains.json deleted file mode 100644 index dc34a948f..000000000 --- a/providers/dns/alwaysdata/internal/fixtures/domains.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "id": 132, - "name": "example.com", - "annotation": "test" - }, - { - "id": 133, - "name": "example.net", - "is_internal": true - }, - { - "id": 134, - "name": "example.org" - } -] diff --git a/providers/dns/alwaysdata/internal/fixtures/record_add-request.json b/providers/dns/alwaysdata/internal/fixtures/record_add-request.json deleted file mode 100644 index 5b6db2646..000000000 --- a/providers/dns/alwaysdata/internal/fixtures/record_add-request.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": 132, - "name": "_acme-challenge", - "type": "TXT", - "value": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "ttl": 120, - "annotation": "lego" -} diff --git a/providers/dns/alwaysdata/internal/fixtures/records.json b/providers/dns/alwaysdata/internal/fixtures/records.json deleted file mode 100644 index fa207395a..000000000 --- a/providers/dns/alwaysdata/internal/fixtures/records.json +++ /dev/null @@ -1,28 +0,0 @@ -[ - { - "id": 789, - "domain": { - "href": "/v1/domain/132/" - }, - "name": "_acme-challenge", - "type": "TXT", - "value": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "ttl": 120, - "annotation": "lego" - }, - { - "id": 11619270, - "domain": { - "href": "/v1/domain/118935/" - }, - "type": "A", - "name": "home", - "value": "149.202.90.65", - "priority": null, - "ttl": 300, - "href": "/v1/record/11619270/", - "annotation": "", - "is_user_defined": true, - "is_active": true - } -] diff --git a/providers/dns/alwaysdata/internal/types.go b/providers/dns/alwaysdata/internal/types.go deleted file mode 100644 index b1e66fa5b..000000000 --- a/providers/dns/alwaysdata/internal/types.go +++ /dev/null @@ -1,33 +0,0 @@ -package internal - -type RecordRequest struct { - ID int64 `json:"id,omitempty"` - DomainID int64 `json:"domain,omitempty"` - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` - Value string `json:"value,omitempty"` - TTL int `json:"ttl,omitempty"` - Annotation string `json:"annotation,omitempty"` - IsUserDefined bool `json:"is_user_defined,omitempty"` - IsActive bool `json:"is_active,omitempty"` -} - -type Record struct { - ID int64 `json:"id,omitempty"` - Domain *Domain `json:"domain,omitempty"` - Type string `json:"type,omitempty"` - Name string `json:"name,omitempty"` - Value string `json:"value,omitempty"` - TTL int `json:"ttl,omitempty"` - Annotation string `json:"annotation,omitempty"` - IsUserDefined bool `json:"is_user_defined,omitempty"` - IsActive bool `json:"is_active,omitempty"` -} - -type Domain struct { - ID int64 `json:"id,omitempty"` - Href string `json:"href,omitempty"` - Name string `json:"name,omitempty"` - IsInternal bool `json:"is_internal,omitempty"` - Annotation string `json:"annotation,omitempty"` -} diff --git a/providers/dns/anexia/anexia.go b/providers/dns/anexia/anexia.go deleted file mode 100644 index 3ce7e2208..000000000 --- a/providers/dns/anexia/anexia.go +++ /dev/null @@ -1,237 +0,0 @@ -// Package anexia implements a DNS provider for solving the DNS-01 challenge using Anexia CloudDNS. -package anexia - -import ( - "context" - "errors" - "fmt" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/cenkalti/backoff/v5" - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/anexia/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "ANEXIA_" - - EnvToken = envNamespace + "TOKEN" - EnvAPIURL = envNamespace + "API_URL" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -const defaultTTL = 300 - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - Token string - APIURL string - - TTL int - PropagationTimeout time.Duration - PollingInterval time.Duration - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, defaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for Anexia CloudDNS. -// Credentials must be passed in the environment variable: ANEXIA_TOKEN. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvToken) - if err != nil { - return nil, fmt.Errorf("anexia: %w", err) - } - - config := NewDefaultConfig() - config.Token = values[EnvToken] - config.APIURL = env.GetOrFile(EnvAPIURL) - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Anexia CloudDNS. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("anexia: the configuration of the DNS provider is nil") - } - - if config.Token == "" { - return nil, errors.New("anexia: incomplete credentials, missing token") - } - - client, err := internal.NewClient(config.Token) - if err != nil { - return nil, fmt.Errorf("anexia: %w", err) - } - - if config.APIURL != "" { - var err error - - client.BaseURL, err = url.Parse(config.APIURL) - if err != nil { - return nil, fmt.Errorf("anexia: %w", err) - } - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -// Present creates a TXT record to fulfill the dns-01 challenge. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("anexia: could not find zone for domain %q: %w", domain, err) - } - - recordName, err := extractRecordName(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("anexia: %w", err) - } - - zoneName := dns01.UnFqdn(authZone) - - recordReq := internal.Record{ - Name: recordName, - Type: "TXT", - RData: info.Value, - TTL: d.config.TTL, - } - - // Ignores returned zone, because of UUID unstability. - // https://github.com/go-acme/lego/pull/2675#issuecomment-3418678194 - _, err = d.client.CreateRecord(ctx, zoneName, recordReq) - if err != nil { - return fmt.Errorf("anexia: new record: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("anexia: could not find zone for domain %q: %w", domain, err) - } - - recordName, err := extractRecordName(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("anexia: %w", err) - } - - recordID, err := d.findRecordID(ctx, dns01.UnFqdn(authZone), recordName, info.Value) - if err != nil { - return fmt.Errorf("anexia: %w", err) - } - - err = d.client.DeleteRecord(ctx, dns01.UnFqdn(authZone), recordID) - if err != nil { - return fmt.Errorf("anexia: delete TXT record: %w", err) - } - - return nil -} - -// findRecordID attempts to find the record ID from the zone response. -// If the record is not immediately available in the response, it retries by querying the zone. -func (d *DNSProvider) findRecordID(ctx context.Context, zoneName, recordName, rdata string) (string, error) { - return backoff.Retry(ctx, - func() (string, error) { - currentZone, err := d.client.GetZone(ctx, zoneName) - if err != nil { - return "", backoff.Permanent(fmt.Errorf("get zone: %w", err)) - } - - recordID := findRecordIdentifier(currentZone, recordName, rdata) - if recordID == "" { - return "", fmt.Errorf("get record identifier: %w", err) - } - - return recordID, nil - }, - backoff.WithBackOff(backoff.NewConstantBackOff(5*time.Second)), - backoff.WithMaxElapsedTime(300*time.Second), - ) -} - -func findRecordIdentifier(zone *internal.Zone, recordName, rdata string) string { - if len(zone.Revisions) == 0 { - return "" - } - - // Check the first revision (index 0) which should be the current one - - for _, record := range zone.Revisions[0].Records { - if record.Name != recordName || record.Type != "TXT" { - continue - } - - if record.RData == rdata || record.RData == strconv.Quote(rdata) { - return record.Identifier - } - } - - return "" -} - -func extractRecordName(fqdn, authZone string) (string, error) { - if dns01.UnFqdn(fqdn) == dns01.UnFqdn(authZone) { - // "@" for the root domain instead of an empty string. - return "@", nil - } - - return dns01.ExtractSubDomain(fqdn, authZone) -} diff --git a/providers/dns/anexia/anexia.toml b/providers/dns/anexia/anexia.toml deleted file mode 100644 index 332f0b8b1..000000000 --- a/providers/dns/anexia/anexia.toml +++ /dev/null @@ -1,31 +0,0 @@ -Name = "Anexia CloudDNS" -Description = '''''' -URL = "https://www.anexia-it.com/" -Code = "anexia" -Since = "v4.28.0" - -Example = ''' -ANEXIA_TOKEN=xxx \ -lego --dns anexia -d '*.example.com' -d example.com run -''' - -Additional = ''' -## Description - -You need to create an API token in the [Anexia Engine](https://engine.anexia-it.com/). - -The token must have permissions to manage DNS zones and records. -''' - -[Configuration] - [Configuration.Credentials] - ANEXIA_TOKEN = "API token for Anexia Engine" - [Configuration.Additional] - ANEXIA_API_URL = "API endpoint URL (default: https://engine.anexia-it.com)" - ANEXIA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - ANEXIA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 300)" - ANEXIA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - ANEXIA_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://engine.anexia-it.com/docs/en/module/clouddns/api" diff --git a/providers/dns/anexia/anexia_test.go b/providers/dns/anexia/anexia_test.go deleted file mode 100644 index 9960c14d1..000000000 --- a/providers/dns/anexia/anexia_test.go +++ /dev/null @@ -1,168 +0,0 @@ -package anexia - -import ( - "net/http/httptest" - "testing" - "time" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest( - EnvToken, - EnvAPIURL). - WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success with token", - envVars: map[string]string{ - EnvToken: "secret", - }, - }, - { - desc: "missing token", - envVars: map[string]string{ - EnvToken: "", - }, - expected: "anexia: some credentials information are missing: ANEXIA_TOKEN", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - assert.NotNil(t, p.config) - assert.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - token string - expected string - }{ - { - desc: "success with token", - token: "secret", - }, - { - desc: "missing token", - token: "", - expected: "anexia: incomplete credentials, missing token", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.Token = test.token - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - assert.NotNil(t, p.config) - assert.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - time.Sleep(2 * time.Second) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.Token = "secret" - config.APIURL = server.URL - config.HTTPClient = server.Client() - - return NewDNSProviderConfig(config) - }, - servermock.CheckHeader(). - WithAuthorization("Token secret"), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("POST /api/clouddns/v1/zone.json/example.com/records", - servermock.ResponseFromInternal("create_record.json"), - servermock.CheckHeader(). - WithContentType("application/json; charset=utf-8"), - servermock.CheckRequestJSONBodyFromInternal("create_record-request.json")). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("GET /api/clouddns/v1/zone.json/example.com", - servermock.ResponseFromInternal("get_zone.json")). - Route("DELETE /api/clouddns/v1/zone.json/example.com/records/12345678-1234-1234-1234-123456789abc", - servermock.Noop()). - Build(t) - - err := provider.CleanUp("example.com", "abc", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/anexia/internal/client.go b/providers/dns/anexia/internal/client.go deleted file mode 100644 index 1a4159be0..000000000 --- a/providers/dns/anexia/internal/client.go +++ /dev/null @@ -1,158 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" -) - -const defaultBaseURL = "https://engine.anexia-it.com" - -// Client the Anexia CloudDNS API client. -type Client struct { - token string - - BaseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(token string) (*Client, error) { - if token == "" { - return nil, errors.New("credentials missing") - } - - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - token: token, - BaseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -func (c *Client) CreateRecord(ctx context.Context, zoneName string, record Record) (*Zone, error) { - endpoint := c.BaseURL.JoinPath("api", "clouddns", "v1", "zone.json", zoneName, "records") - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) - if err != nil { - return nil, err - } - - var zone Zone - - err = c.do(req, &zone) - if err != nil { - return nil, err - } - - return &zone, nil -} - -func (c *Client) DeleteRecord(ctx context.Context, zoneName, recordID string) error { - endpoint := c.BaseURL.JoinPath("api", "clouddns", "v1", "zone.json", zoneName, "records", recordID) - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return err - } - - return c.do(req, nil) -} - -func (c *Client) GetZone(ctx context.Context, zoneName string) (*Zone, error) { - endpoint := c.BaseURL.JoinPath("api", "clouddns", "v1", "zone.json", zoneName) - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var zone Zone - - err = c.do(req, &zone) - if err != nil { - return nil, err - } - - return &zone, nil -} - -func (c *Client) do(req *http.Request, result any) error { - useragent.SetHeader(req.Header) - - req.Header.Add("Authorization", fmt.Sprintf("Token %s", c.token)) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json; charset=utf-8") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIError - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return &errAPI -} diff --git a/providers/dns/anexia/internal/client_test.go b/providers/dns/anexia/internal/client_test.go deleted file mode 100644 index be33d6f88..000000000 --- a/providers/dns/anexia/internal/client_test.go +++ /dev/null @@ -1,133 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("secret") - if err != nil { - return nil, err - } - - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader(). - WithAuthorization("Token secret"), - ) -} - -func TestClient_CreateRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /api/clouddns/v1/zone.json/example.com/records", - servermock.ResponseFromFixture("create_record.json"), - servermock.CheckHeader(). - WithContentType("application/json; charset=utf-8"), - servermock.CheckRequestJSONBodyFromFixture("create_record-request.json")). - Build(t) - - record := Record{ - Name: "_acme-challenge", - RData: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: 300, - Type: "TXT", - } - - zone, err := client.CreateRecord(t.Context(), "example.com", record) - require.NoError(t, err) - - expected := &Zone{ - Name: "example.com", - TTL: 86400, - ZoneName: "example.com", - Revisions: []Revision{{ - Identifier: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", - Records: []Record{{ - Identifier: "12345678-1234-1234-1234-123456789abc", - Name: "_acme-challenge", - RData: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: 300, - Type: "TXT", - }}, - State: "deployed", - }}, - } - - assert.Equal(t, expected, zone) -} - -func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /api/clouddns/v1/zone.json/example.com/records/12345678-1234-1234-1234-123456789abc", - servermock.Noop()). - Build(t) - - err := client.DeleteRecord(t.Context(), "example.com", "12345678-1234-1234-1234-123456789abc") - require.NoError(t, err) -} - -func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /api/clouddns/v1/zone.json/example.com/records/12345678-1234-1234-1234-123456789abc", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) - - err := client.DeleteRecord(t.Context(), "example.com", "12345678-1234-1234-1234-123456789abc") - require.EqualError(t, err, "401: Unauthorized") -} - -func TestClient_GetZone(t *testing.T) { - client := mockBuilder(). - Route("GET /api/clouddns/v1/zone.json/example.com", - servermock.ResponseFromFixture("get_zone.json")). - Build(t) - - zone, err := client.GetZone(t.Context(), "example.com") - require.NoError(t, err) - - expected := &Zone{ - Identifier: "fdb355ffd07c48aba3d4f6bf6a116296", - Name: "example.com", - TTL: 3600, - ZoneName: "", - Revisions: []Revision{{ - Identifier: "eeed7e08-f1ad-442b-9e75-369a0958c7d8", - Records: []Record{ - { - Identifier: "5ced498b-c89d-4487-824d-c03ded84f849", - Immutable: true, - Name: "@", - RData: "acns02.xaas.systems.", - Region: "9a1609af9dae4ce1a4ef63f51d305321", - TTL: 3600, - Type: "NS", - }, - { - Identifier: "12345678-1234-1234-1234-123456789abc", - Immutable: false, - Name: "_acme-challenge", - RData: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - Region: "", - TTL: 300, - Type: "TXT", - }, - }, - State: "active", - }}, - } - - assert.Equal(t, expected, zone) -} diff --git a/providers/dns/anexia/internal/fixtures/create_record-request.json b/providers/dns/anexia/internal/fixtures/create_record-request.json deleted file mode 100644 index e82add260..000000000 --- a/providers/dns/anexia/internal/fixtures/create_record-request.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "_acme-challenge", - "type": "TXT", - "rdata": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "region": "", - "ttl": 300 -} diff --git a/providers/dns/anexia/internal/fixtures/create_record.json b/providers/dns/anexia/internal/fixtures/create_record.json deleted file mode 100644 index 8c4f2c149..000000000 --- a/providers/dns/anexia/internal/fixtures/create_record.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "example.com", - "zone_name": "example.com", - "master": true, - "dnssec_mode": "managed", - "admin_email": "admin@example.com", - "refresh": 10800, - "retry": 3600, - "expire": 604800, - "ttl": 86400, - "customer": "ANX12345", - "created_at": "0001-01-01T00:00:00Z", - "updated_at": "0001-01-01T00:00:00Z", - "published_at": "0001-01-01T00:00:00Z", - "is_editable": true, - "validation_level": 0, - "deployment_level": 0, - "revisions": [ - { - "created_at": "0001-01-01T00:00:00Z", - "identifier": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", - "modified_at": "0001-01-01T00:00:00Z", - "records": [ - { - "identifier": "12345678-1234-1234-1234-123456789abc", - "immutable": false, - "name": "_acme-challenge", - "rdata": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "region": "", - "ttl": 300, - "type": "TXT" - } - ], - "serial": 1, - "state": "deployed" - } - ] -} diff --git a/providers/dns/anexia/internal/fixtures/create_record_incomplete.json b/providers/dns/anexia/internal/fixtures/create_record_incomplete.json deleted file mode 100644 index 0515fcde3..000000000 --- a/providers/dns/anexia/internal/fixtures/create_record_incomplete.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "example.com", - "zone_name": "example.com", - "master": true, - "dnssec_mode": "managed", - "admin_email": "admin@example.com", - "refresh": 10800, - "retry": 3600, - "expire": 604800, - "ttl": 86400, - "customer": "ANX12345", - "created_at": "0001-01-01T00:00:00Z", - "updated_at": "0001-01-01T00:00:00Z", - "published_at": "0001-01-01T00:00:00Z", - "is_editable": true, - "validation_level": 0, - "deployment_level": 0, - "revisions": [ - { - "created_at": "0001-01-01T00:00:00Z", - "identifier": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", - "modified_at": "0001-01-01T00:00:00Z", - "records": [ - { - "immutable": false, - "name": "_acme-challenge", - "rdata": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "region": "", - "ttl": 300, - "type": "TXT" - } - ], - "serial": 1, - "state": "deployed" - } - ] -} diff --git a/providers/dns/anexia/internal/fixtures/error.json b/providers/dns/anexia/internal/fixtures/error.json deleted file mode 100644 index afed571fa..000000000 --- a/providers/dns/anexia/internal/fixtures/error.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "error": { - "code": 401, - "message": "Unauthorized" - } -} diff --git a/providers/dns/anexia/internal/fixtures/get_zone.json b/providers/dns/anexia/internal/fixtures/get_zone.json deleted file mode 100644 index 6e54594ff..000000000 --- a/providers/dns/anexia/internal/fixtures/get_zone.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "identifier": "fdb355ffd07c48aba3d4f6bf6a116296", - "admin_email": "admin@example.com", - "created_at": "2019-02-06T10:02:07.000Z", - "current_revision": "eeed7e08-f1ad-442b-9e75-369a0958c7d8", - "deployment_level": 100, - "dns_servers": [ - { - "server": "acns01.xaas.systems", - "alias": null - }, - { - "server": "acns04.xaas.systems", - "alias": null - }, - { - "server": "acns02.xaas.systems", - "alias": null - }, - { - "server": "acns03.xaas.systems", - "alias": null - }, - { - "server": "acns05.xaas.systems", - "alias": null - } - ], - "dnsCluster": null, - "dnssec_ksk": null, - "dnssec_mode": "unvalidated", - "dnssec_sig_expires_at": null, - "dnssec_zsk": null, - "expire": 604800, - "inherit_ns_from": null, - "nameserver_set": null, - "master": true, - "master_ns": "acns02.xaas.systems.", - "name": "example.com", - "notify_allowed_ips": [ - "127.0.0.1" - ], - "published_at": "2023-06-20T08:41:06.000Z", - "refresh": 14400, - "revisions": [ - { - "created_at": "2023-06-20T08:41:06.000000Z", - "identifier": "eeed7e08-f1ad-442b-9e75-369a0958c7d8", - "modified_at": "2023-06-20T08:41:06.000000Z", - "records": [ - { - "identifier": "5ced498b-c89d-4487-824d-c03ded84f849", - "immutable": true, - "name": "@", - "rdata": "acns02.xaas.systems.", - "region": "9a1609af9dae4ce1a4ef63f51d305321", - "ttl": 3600, - "type": "NS", - "options": null - }, - { - "identifier": "12345678-1234-1234-1234-123456789abc", - "immutable": false, - "name": "_acme-challenge", - "rdata": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "region": "", - "ttl": 300, - "Type": "TXT" - } - ], - "serial": 14, - "state": "active" - } - ], - "retry": 3600, - "ttl": 3600, - "updated_at": "2020-06-04T18:34:22.000Z", - "validation_level": 100, - "whitelabel_config": null, - "is_editable": true, - "deploy_zone": "49459f420f614eb2a979fc7e961f83e6" -} diff --git a/providers/dns/anexia/internal/types.go b/providers/dns/anexia/internal/types.go deleted file mode 100644 index f5546ca98..000000000 --- a/providers/dns/anexia/internal/types.go +++ /dev/null @@ -1,38 +0,0 @@ -package internal - -import "fmt" - -type APIError struct { - Details struct { - Code int `json:"code"` - Message string `json:"message"` - } `json:"error"` -} - -func (a *APIError) Error() string { - return fmt.Sprintf("%d: %s", a.Details.Code, a.Details.Message) -} - -type Zone struct { - Identifier string `json:"identifier,omitempty"` - Name string `json:"name,omitempty"` - TTL int `json:"ttl,omitempty"` - ZoneName string `json:"zone_name,omitempty"` - Revisions []Revision `json:"revisions,omitempty"` -} - -type Revision struct { - Identifier string `json:"identifier,omitempty"` - Records []Record `json:"records,omitempty"` - State string `json:"state,omitempty"` -} - -type Record struct { - Identifier string `json:"identifier,omitempty"` - Immutable bool `json:"immutable,omitempty"` - Name string `json:"name,omitempty"` - RData string `json:"rdata,omitempty"` - Region string `json:"region"` - TTL int `json:"ttl,omitempty"` - Type string `json:"type,omitempty"` -} diff --git a/providers/dns/artfiles/artfiles.go b/providers/dns/artfiles/artfiles.go deleted file mode 100644 index c918d77f6..000000000 --- a/providers/dns/artfiles/artfiles.go +++ /dev/null @@ -1,204 +0,0 @@ -// Package artfiles implements a DNS provider for solving the DNS-01 challenge using ArtFiles. -package artfiles - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net/http" - "slices" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/artfiles/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "ARTFILES_" - - EnvUsername = envNamespace + "USERNAME" - EnvPassword = envNamespace + "PASSWORD" - - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - Username string - Password string - - PropagationTimeout time.Duration - PollingInterval time.Duration - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 6*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for ArtFiles. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvUsername, EnvPassword) - if err != nil { - return nil, fmt.Errorf("artfiles: %w", err) - } - - config := NewDefaultConfig() - config.Username = values[EnvUsername] - config.Password = values[EnvPassword] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for ArtFiles. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("artfiles: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.Username, config.Password) - if err != nil { - return nil, fmt.Errorf("artfiles: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - zone, err := d.findZone(ctx, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("artfiles: %w", err) - } - - records, err := d.client.GetRecords(ctx, zone) - if err != nil { - return fmt.Errorf("artfiles: get records: %w", err) - } - - rv := internal.RecordValue{} - - if len(records["TXT"]) > 0 { - var raw string - - err = json.Unmarshal(records["TXT"], &raw) - if err != nil { - return fmt.Errorf("artfiles: unmarshal TXT records: %w", err) - } - - rv = internal.ParseRecordValue(raw) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone) - if err != nil { - return fmt.Errorf("artfiles: %w", err) - } - - rv.Add(subDomain, info.Value) - - err = d.client.SetRecords(ctx, zone, "TXT", rv) - if err != nil { - return fmt.Errorf("artfiles: set TXT records: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - zone, err := d.findZone(ctx, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("artfiles: %w", err) - } - - records, err := d.client.GetRecords(ctx, zone) - if err != nil { - return fmt.Errorf("artfiles: get records: %w", err) - } - - var raw string - - err = json.Unmarshal(records["TXT"], &raw) - if err != nil { - return fmt.Errorf("artfiles: unmarshal TXT records: %w", err) - } - - rv := internal.ParseRecordValue(raw) - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone) - if err != nil { - return fmt.Errorf("artfiles: %w", err) - } - - rv.RemoveValue(subDomain, info.Value) - - err = d.client.SetRecords(ctx, zone, "TXT", rv) - if err != nil { - return fmt.Errorf("artfiles: set TXT records: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -func (d *DNSProvider) findZone(ctx context.Context, fqdn string) (string, error) { - domains, err := d.client.GetDomains(ctx) - if err != nil { - return "", fmt.Errorf("artfiles: get domains: %w", err) - } - - var zone string - - for s := range dns01.UnFqdnDomainsSeq(fqdn) { - if slices.Contains(domains, s) { - zone = s - } - } - - if zone == "" { - return "", fmt.Errorf("artfiles: could not find the zone for domain %q", fqdn) - } - - return zone, nil -} diff --git a/providers/dns/artfiles/artfiles.toml b/providers/dns/artfiles/artfiles.toml deleted file mode 100644 index 00ff12342..000000000 --- a/providers/dns/artfiles/artfiles.toml +++ /dev/null @@ -1,24 +0,0 @@ -Name = "ArtFiles" -Description = '''''' -URL = "https://www.artfiles.de/extras/domains/" -Code = "artfiles" -Since = "v4.32.0" - -Example = ''' -ARTFILES_USERNAME="xxx" \ -ARTFILES_PASSWORD="yyy" \ -lego --dns artfiles -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - ARTFILES_USERNAME = "API username" - ARTFILES_PASSWORD = "API password" - [Configuration.Additional] - ARTFILES_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - ARTFILES_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 360)" - ARTFILES_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - ARTFILES_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://support.artfiles.de/DCP-API#dns" diff --git a/providers/dns/artfiles/artfiles_test.go b/providers/dns/artfiles/artfiles_test.go deleted file mode 100644 index 42490f10d..000000000 --- a/providers/dns/artfiles/artfiles_test.go +++ /dev/null @@ -1,228 +0,0 @@ -package artfiles - -import ( - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvUsername, EnvPassword).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvUsername: "user", - EnvPassword: "secret", - }, - }, - { - desc: "missing username", - envVars: map[string]string{ - EnvUsername: "", - EnvPassword: "secret", - }, - expected: "artfiles: some credentials information are missing: ARTFILES_USERNAME", - }, - { - desc: "missing password", - envVars: map[string]string{ - EnvUsername: "user", - EnvPassword: "", - }, - expected: "artfiles: some credentials information are missing: ARTFILES_PASSWORD", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "artfiles: some credentials information are missing: ARTFILES_USERNAME,ARTFILES_PASSWORD", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - username string - password string - expected string - }{ - { - desc: "success", - username: "user", - password: "secret", - }, - { - desc: "missing username", - password: "secret", - expected: "artfiles: credentials missing", - }, - { - desc: "missing Example", - username: "user", - expected: "artfiles: credentials missing", - }, - { - desc: "missing credentials", - expected: "artfiles: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.Username = test.username - config.Password = test.password - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.Username = "user" - config.Password = "secret" - config.HTTPClient = server.Client() - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - p.client.BaseURL, _ = url.Parse(server.URL) - - return p, nil - }, - servermock.CheckHeader(). - WithBasicAuth("user", "secret"), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("GET /domain/get_domains.html", - servermock.ResponseFromInternal("domains.txt"), - ). - Route("GET /dns/get_dns.html", - servermock.ResponseFromInternal("get_dns.json"), - servermock.CheckQueryParameter().Strict(). - With("domain", "example.com"), - ). - Route("POST /dns/set_dns.html", - servermock.ResponseFromInternal("set_dns.json"), - servermock.CheckQueryParameter().Strict(). - With("TXT", `@ "v=spf1 a mx ~all" -_acme-challenge "TheAcmeChallenge" -_acme-challenge "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" -_dmarc "v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf" -_mta-sts "v=STSv1;id=yyyymmddTHHMMSS;" -_smtp._tls "v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com" -selector._domainkey "v=DKIM1;k=rsa;p=Base64Stuff" "MoreBase64Stuff" "Even++MoreBase64Stuff" "YesMoreBase64Stuff" "And+Yes+Even+MoreBase64Stuff" "Sure++MoreBase64Stuff" "LastBase64Stuff" -selectorecc._domainkey "v=DKIM1;k=ed25519;p=Base64Stuff"`). - With("domain", "example.com"), - ). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("GET /domain/get_domains.html", - servermock.ResponseFromInternal("domains.txt"), - ). - Route("GET /dns/get_dns.html", - servermock.ResponseFromInternal("get_dns.json"), - servermock.CheckQueryParameter().Strict(). - With("domain", "example.com"), - ). - Route("POST /dns/set_dns.html", - servermock.ResponseFromInternal("set_dns.json"), - servermock.CheckQueryParameter().Strict(). - With("TXT", `@ "v=spf1 a mx ~all" -_acme-challenge "TheAcmeChallenge" -_dmarc "v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf" -_mta-sts "v=STSv1;id=yyyymmddTHHMMSS;" -_smtp._tls "v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com" -selector._domainkey "v=DKIM1;k=rsa;p=Base64Stuff" "MoreBase64Stuff" "Even++MoreBase64Stuff" "YesMoreBase64Stuff" "And+Yes+Even+MoreBase64Stuff" "Sure++MoreBase64Stuff" "LastBase64Stuff" -selectorecc._domainkey "v=DKIM1;k=ed25519;p=Base64Stuff"`). - With("domain", "example.com"), - ). - Build(t) - - err := provider.CleanUp("example.com", "abc", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/artfiles/internal/client.go b/providers/dns/artfiles/internal/client.go deleted file mode 100644 index 61b350511..000000000 --- a/providers/dns/artfiles/internal/client.go +++ /dev/null @@ -1,133 +0,0 @@ -package internal - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" -) - -const defaultBaseURL = "https://dcp.c.artfiles.de/api/" - -// Client the ArtFiles API client. -type Client struct { - username string - password string - - BaseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(username, password string) (*Client, error) { - if username == "" || password == "" { - return nil, errors.New("credentials missing") - } - - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - username: username, - password: password, - BaseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -func (c *Client) GetDomains(ctx context.Context) ([]string, error) { - endpoint := c.BaseURL.JoinPath("domain", "get_domains.html") - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - raw, err := c.do(req) - if err != nil { - return nil, err - } - - return parseDomains(string(raw)) -} - -func (c *Client) GetRecords(ctx context.Context, domain string) (map[string]json.RawMessage, error) { - endpoint := c.BaseURL.JoinPath("dns", "get_dns.html") - - query := endpoint.Query() - query.Set("domain", domain) - - endpoint.RawQuery = query.Encode() - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - raw, err := c.do(req) - if err != nil { - return nil, err - } - - var result Records - - err = json.Unmarshal(raw, &result) - if err != nil { - return nil, errutils.NewUnmarshalError(req, http.StatusOK, raw, err) - } - - return result.Data, nil -} - -func (c *Client) SetRecords(ctx context.Context, domain, rType string, value RecordValue) error { - endpoint := c.BaseURL.JoinPath("dns", "set_dns.html") - - query := endpoint.Query() - query.Set("domain", domain) - query.Set(rType, value.String()) - - endpoint.RawQuery = query.Encode() - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), nil) - if err != nil { - return fmt.Errorf("unable to create request: %w", err) - } - - _, err = c.do(req) - - return err -} - -func (c *Client) do(req *http.Request) ([]byte, error) { - useragent.SetHeader(req.Header) - - req.SetBasicAuth(c.username, c.password) - - if req.Method == http.MethodPost { - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - } - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return nil, errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return nil, errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - if resp.StatusCode/100 != 2 { - return nil, errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return raw, nil -} diff --git a/providers/dns/artfiles/internal/client_test.go b/providers/dns/artfiles/internal/client_test.go deleted file mode 100644 index cc76f06f5..000000000 --- a/providers/dns/artfiles/internal/client_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package internal - -import ( - "encoding/json" - "net/http/httptest" - "net/url" - "strconv" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("user", "secret") - if err != nil { - return nil, err - } - - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader(). - WithBasicAuth("user", "secret"), - ) -} - -func TestClient_GetDomains(t *testing.T) { - client := mockBuilder(). - Route("GET /domain/get_domains.html", - servermock.ResponseFromFixture("domains.txt"), - ). - Build(t) - - zones, err := client.GetDomains(t.Context()) - require.NoError(t, err) - - expected := []string{"example.com", "example.org", "example.net"} - - assert.Equal(t, expected, zones) -} - -func TestClient_GetRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/get_dns.html", - servermock.ResponseFromFixture("get_dns.json"), - servermock.CheckQueryParameter().Strict(). - With("domain", "example.com"), - ). - Build(t) - - records, err := client.GetRecords(t.Context(), "example.com") - require.NoError(t, err) - - expected := map[string]json.RawMessage{ - "A": json.RawMessage(strconv.Quote("sub1 1.2.3.4\nsub2 1.2.3.4\nsub3 1.2.3.4\nsub4 1.2.3.4\nsub5 1.2.3.4\nsub6 1.2.3.4\nsub7 1.2.3.4\nsub8 1.2.3.4\nsub9 1.2.3.4\nsub10 1.2.3.4\nsub11 1.2.3.4\nsub12 1.2.3.4\nsub13 1.2.3.4\nsub14 1.2.3.4\nsub15 1.2.3.4\nsub16 1.2.3.4\nsub17 1.2.3.4\nsub18 1.2.3.4\n@ 1.2.3.4")), - "AAAA": json.RawMessage(strconv.Quote("")), - "CAA": json.RawMessage(strconv.Quote("@ 128 iodef \"mailto:someone@example.tld\"\n@ 128 issue \"letsencrypt.org\"\n@ 128 issuewild \"letsencrypt.org\"")), - "CName": json.RawMessage(strconv.Quote("some cname.to.example.tld.")), - "MX": json.RawMessage(strconv.Quote("10 mail.example.tld.")), - "SRV": json.RawMessage(strconv.Quote("_imap._tcp 0 0 0 .\n_imaps._tcp 0 1 993 mail.example.tld.\n_pop3._tcp 0 0 0 .\n_pop3s._tcp 0 0 0 .")), - "TLSA": json.RawMessage(strconv.Quote("_25._tcp.mail.example.tld. 2 1 1 CBBC559B44D524D6A132BDAC672744DA3407F12AAE5D5F722C5F6C7913871C75\n_25._tcp.mail.example.tld. 2 1 1 885BF0572252C6741DC9A52F5044487FEF2A93B811CDEDFAD7624CC283B7CDD5\n_25._tcp.mail.example.tld. 2 1 1 F1440A9B76E1E41E53A4CB461329BF6337B419726BE513E42E19F1C691C5D4B2\n_465._tcp.mail.example.tld. 2 1 1 CBBC559B44D524D6A132BDAC672744DA3407F12AAE5D5F722C5F6C7913871C75\n_465._tcp.mail.example.tld. 2 1 1 885BF0572252C6741DC9A52F5044487FEF2A93B811CDEDFAD7624CC283B7CDD5\n_465._tcp.mail.example.tld. 2 1 1 F1440A9B76E1E41E53A4CB461329BF6337B419726BE513E42E19F1C691C5D4B2\n_587._tcp.mail.example.tld. 2 1 1 CBBC559B44D524D6A132BDAC672744DA3407F12AAE5D5F722C5F6C7913871C75\n_587._tcp.mail.example.tld. 2 1 1 885BF0572252C6741DC9A52F5044487FEF2A93B811CDEDFAD7624CC283B7CDD5\n_587._tcp.mail.example.tld. 2 1 1 F1440A9B76E1E41E53A4CB461329BF6337B419726BE513E42E19F1C691C5D4B2")), - "TXT": json.RawMessage(strconv.Quote("_dmarc \"v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf\"\n_mta-sts \"v=STSv1;id=yyyymmddTHHMMSS;\"\n_smtp._tls \"v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com\"\n@ \"v=spf1 a mx ~all\"\nselector._domainkey \"v=DKIM1;k=rsa;p=Base64Stuff\" \"MoreBase64Stuff\" \"Even++MoreBase64Stuff\" \"YesMoreBase64Stuff\" \"And+Yes+Even+MoreBase64Stuff\" \"Sure++MoreBase64Stuff\" \"LastBase64Stuff\"\nselectorecc._domainkey \"v=DKIM1;k=ed25519;p=Base64Stuff\"\n_acme-challenge \"TheAcmeChallenge\"")), - "TTL": json.RawMessage("3600"), - "comment": json.RawMessage(strconv.Quote("TLSA RR:\nInfo -> https://dnssec-stats.ant.isi.edu/~viktor/x3hosts.html\nTest 1 -> https://stats.dnssec-tools.org/explore/?example.tld\nTest 2 -> https://dane.sys4.de/smtp/example.tld\n\nSMIMEA RR:\nGenerator -> https://www.smimea.info/smimea-generator.php\nTest -> https://www.smimea.info/smimea-test.php")), - "nameserver": json.RawMessage(strconv.Quote("auth1.artfiles.de.\nauth2.artfiles.de.")), - } - - assert.Equal(t, expected, records) -} - -func TestClient_SetRecords(t *testing.T) { - client := mockBuilder(). - Route("POST /dns/set_dns.html", - servermock.ResponseFromFixture("set_dns.json"), - servermock.CheckQueryParameter().Strict(). - With("TXT", "a b\nc \"d\""). - With("domain", "example.com"), - ). - Build(t) - - err := client.SetRecords(t.Context(), "example.com", "TXT", RecordValue{"c": []string{`"d"`}, "a": []string{"b"}}) - require.NoError(t, err) -} diff --git a/providers/dns/artfiles/internal/fixtures/domains.txt b/providers/dns/artfiles/internal/fixtures/domains.txt deleted file mode 100644 index b8a1247d2..000000000 --- a/providers/dns/artfiles/internal/fixtures/domains.txt +++ /dev/null @@ -1,3 +0,0 @@ -example.com normal 2026-10-01 2017-09-18 163477 -example.org normal 2026-08-01 2016-07-07 156216 -example.net normal 2026-07-01 2017-06-06 162462 diff --git a/providers/dns/artfiles/internal/fixtures/get_dns.json b/providers/dns/artfiles/internal/fixtures/get_dns.json deleted file mode 100644 index fa672e0e1..000000000 --- a/providers/dns/artfiles/internal/fixtures/get_dns.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "data": { - "SRV": "_imap._tcp 0 0 0 .\n_imaps._tcp 0 1 993 mail.example.tld.\n_pop3._tcp 0 0 0 .\n_pop3s._tcp 0 0 0 .", - "AAAA": "", - "MX": "10 mail.example.tld.", - "CAA": "@ 128 iodef \"mailto:someone@example.tld\"\n@ 128 issue \"letsencrypt.org\"\n@ 128 issuewild \"letsencrypt.org\"", - "TTL": 3600, - "comment": "TLSA RR:\nInfo -> https://dnssec-stats.ant.isi.edu/~viktor/x3hosts.html\nTest 1 -> https://stats.dnssec-tools.org/explore/?example.tld\nTest 2 -> https://dane.sys4.de/smtp/example.tld\n\nSMIMEA RR:\nGenerator -> https://www.smimea.info/smimea-generator.php\nTest -> https://www.smimea.info/smimea-test.php", - "TXT": "_dmarc \"v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf\"\n_mta-sts \"v=STSv1;id=yyyymmddTHHMMSS;\"\n_smtp._tls \"v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com\"\n@ \"v=spf1 a mx ~all\"\nselector._domainkey \"v=DKIM1;k=rsa;p=Base64Stuff\" \"MoreBase64Stuff\" \"Even++MoreBase64Stuff\" \"YesMoreBase64Stuff\" \"And+Yes+Even+MoreBase64Stuff\" \"Sure++MoreBase64Stuff\" \"LastBase64Stuff\"\nselectorecc._domainkey \"v=DKIM1;k=ed25519;p=Base64Stuff\"\n_acme-challenge \"TheAcmeChallenge\"", - "A": "sub1 1.2.3.4\nsub2 1.2.3.4\nsub3 1.2.3.4\nsub4 1.2.3.4\nsub5 1.2.3.4\nsub6 1.2.3.4\nsub7 1.2.3.4\nsub8 1.2.3.4\nsub9 1.2.3.4\nsub10 1.2.3.4\nsub11 1.2.3.4\nsub12 1.2.3.4\nsub13 1.2.3.4\nsub14 1.2.3.4\nsub15 1.2.3.4\nsub16 1.2.3.4\nsub17 1.2.3.4\nsub18 1.2.3.4\n@ 1.2.3.4", - "nameserver": "auth1.artfiles.de.\nauth2.artfiles.de.", - "CName": "some cname.to.example.tld.", - "TLSA": "_25._tcp.mail.example.tld. 2 1 1 CBBC559B44D524D6A132BDAC672744DA3407F12AAE5D5F722C5F6C7913871C75\n_25._tcp.mail.example.tld. 2 1 1 885BF0572252C6741DC9A52F5044487FEF2A93B811CDEDFAD7624CC283B7CDD5\n_25._tcp.mail.example.tld. 2 1 1 F1440A9B76E1E41E53A4CB461329BF6337B419726BE513E42E19F1C691C5D4B2\n_465._tcp.mail.example.tld. 2 1 1 CBBC559B44D524D6A132BDAC672744DA3407F12AAE5D5F722C5F6C7913871C75\n_465._tcp.mail.example.tld. 2 1 1 885BF0572252C6741DC9A52F5044487FEF2A93B811CDEDFAD7624CC283B7CDD5\n_465._tcp.mail.example.tld. 2 1 1 F1440A9B76E1E41E53A4CB461329BF6337B419726BE513E42E19F1C691C5D4B2\n_587._tcp.mail.example.tld. 2 1 1 CBBC559B44D524D6A132BDAC672744DA3407F12AAE5D5F722C5F6C7913871C75\n_587._tcp.mail.example.tld. 2 1 1 885BF0572252C6741DC9A52F5044487FEF2A93B811CDEDFAD7624CC283B7CDD5\n_587._tcp.mail.example.tld. 2 1 1 F1440A9B76E1E41E53A4CB461329BF6337B419726BE513E42E19F1C691C5D4B2" - }, - "status": "OK" -} diff --git a/providers/dns/artfiles/internal/fixtures/set_dns.json b/providers/dns/artfiles/internal/fixtures/set_dns.json deleted file mode 100644 index 7cacb33e5..000000000 --- a/providers/dns/artfiles/internal/fixtures/set_dns.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "status": "OK", - "error": "" -} diff --git a/providers/dns/artfiles/internal/fixtures/txt_record-multiple.txt b/providers/dns/artfiles/internal/fixtures/txt_record-multiple.txt deleted file mode 100644 index 461489c77..000000000 --- a/providers/dns/artfiles/internal/fixtures/txt_record-multiple.txt +++ /dev/null @@ -1,8 +0,0 @@ -_dmarc "v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf" -_mta-sts "v=STSv1;id=yyyymmddTHHMMSS;" -_smtp._tls "v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com" -@ "v=spf1 a mx ~all" -selector._domainkey "v=DKIM1;k=rsa;p=Base64Stuff" "MoreBase64Stuff" "Even++MoreBase64Stuff" "YesMoreBase64Stuff" "And+Yes+Even+MoreBase64Stuff" "Sure++MoreBase64Stuff" "LastBase64Stuff" -selectorecc._domainkey "v=DKIM1;k=ed25519;p=Base64Stuff" -_acme-challenge "xxx" -_acme-challenge "yyy" diff --git a/providers/dns/artfiles/internal/fixtures/txt_record.txt b/providers/dns/artfiles/internal/fixtures/txt_record.txt deleted file mode 100644 index 5a6259b14..000000000 --- a/providers/dns/artfiles/internal/fixtures/txt_record.txt +++ /dev/null @@ -1,7 +0,0 @@ -_dmarc "v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf" -_mta-sts "v=STSv1;id=yyyymmddTHHMMSS;" -_smtp._tls "v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com" -@ "v=spf1 a mx ~all" -selector._domainkey "v=DKIM1;k=rsa;p=Base64Stuff" "MoreBase64Stuff" "Even++MoreBase64Stuff" "YesMoreBase64Stuff" "And+Yes+Even+MoreBase64Stuff" "Sure++MoreBase64Stuff" "LastBase64Stuff" -selectorecc._domainkey "v=DKIM1;k=ed25519;p=Base64Stuff" -_acme-challenge "TheAcmeChallenge" diff --git a/providers/dns/artfiles/internal/types.go b/providers/dns/artfiles/internal/types.go deleted file mode 100644 index c70ab34da..000000000 --- a/providers/dns/artfiles/internal/types.go +++ /dev/null @@ -1,109 +0,0 @@ -package internal - -import ( - "encoding/csv" - "encoding/json" - "errors" - "io" - "maps" - "slices" - "strconv" - "strings" - "unicode" -) - -type Records struct { - Data map[string]json.RawMessage `json:"data"` - Status string `json:"status"` -} - -type RecordValue map[string][]string - -func (r RecordValue) Set(key, value string) { - r[key] = []string{strconv.Quote(value)} -} - -func (r RecordValue) Add(key, value string) { - r[key] = append(r[key], strconv.Quote(value)) -} - -func (r RecordValue) Delete(key string) { - delete(r, key) -} - -func (r RecordValue) RemoveValue(key, value string) { - if len(r[key]) == 0 { - return - } - - quotedValue := strconv.Quote(value) - - var data []string - - for _, s := range r[key] { - if s != quotedValue { - data = append(data, s) - } - } - - r[key] = data - - if len(r[key]) == 0 { - r.Delete(key) - } -} - -func (r RecordValue) String() string { - var parts []string - - for _, key := range slices.Sorted(maps.Keys(r)) { - for _, s := range r[key] { - parts = append(parts, key+" "+s) - } - } - - return strings.Join(parts, "\n") -} - -func ParseRecordValue(lines string) RecordValue { - data := make(RecordValue) - - for line := range strings.Lines(lines) { - line = strings.TrimSpace(line) - - idx := strings.IndexFunc(line, unicode.IsSpace) - - data[line[:idx]] = append(data[line[:idx]], line[idx+1:]) - } - - return data -} - -func parseDomains(input string) ([]string, error) { - reader := csv.NewReader(strings.NewReader(input)) - reader.Comma = '\t' - reader.TrimLeadingSpace = true - reader.LazyQuotes = true - - var data []string - - for { - record, err := reader.Read() - if errors.Is(err, io.EOF) { - break - } - - if err != nil { - return nil, err - } - - if len(record) < 1 { - // Malformed line - continue - } - - data = append(data, record[0]) - } - - return data, nil -} diff --git a/providers/dns/artfiles/internal/types_test.go b/providers/dns/artfiles/internal/types_test.go deleted file mode 100644 index 3b219f39f..000000000 --- a/providers/dns/artfiles/internal/types_test.go +++ /dev/null @@ -1,183 +0,0 @@ -package internal - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestRecordValue_Set(t *testing.T) { - rv := make(RecordValue) - - rv.Set("a", "1") - rv.Set("b", "2") - rv.Set("b", "3") - - assert.Equal(t, "a \"1\"\nb \"3\"", rv.String()) -} - -func TestRecordValue_Add(t *testing.T) { - rv := make(RecordValue) - - rv.Add("a", "1") - rv.Add("b", "2") - rv.Add("b", "3") - - assert.Equal(t, "a \"1\"\nb \"2\"\nb \"3\"", rv.String()) -} - -func TestRecordValue_Delete(t *testing.T) { - rv := make(RecordValue) - - rv.Set("a", "1") - rv.Add("b", "2") - - rv.Delete("b") - - assert.Equal(t, "a \"1\"", rv.String()) -} - -func TestRecordValue_RemoveValue(t *testing.T) { - testCases := []struct { - desc string - data map[string][]string - toRemove map[string][]string - expected string - }{ - { - desc: "remove the only value", - data: map[string][]string{ - "a": {"1"}, - }, - toRemove: map[string][]string{ - "a": {"1"}, - }, - expected: ``, - }, - { - desc: "remove value in the middle", - data: map[string][]string{ - "a": {"1", "2", "3"}, - }, - toRemove: map[string][]string{ - "a": {"2"}, - }, - expected: "a \"1\"\na \"3\"", - }, - { - desc: "remove value at the beginning", - data: map[string][]string{ - "a": {"1", "2", "3"}, - }, - toRemove: map[string][]string{ - "a": {"1"}, - }, - expected: "a \"2\"\na \"3\"", - }, - { - desc: "remove value at the end", - data: map[string][]string{ - "a": {"1", "2", "3"}, - }, - toRemove: map[string][]string{ - "a": {"3"}, - }, - expected: "a \"1\"\na \"2\"", - }, - { - desc: "remove all (delete)", - data: map[string][]string{ - "a": {"1", "2", "3"}, - }, - toRemove: map[string][]string{ - "a": {"1", "2", "3"}, - }, - expected: ``, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - rv := make(RecordValue) - - for k, values := range test.data { - for _, v := range values { - rv.Add(k, v) - } - } - - for k, values := range test.toRemove { - for _, v := range values { - rv.RemoveValue(k, v) - } - } - - assert.Equal(t, test.expected, rv.String()) - }) - } -} - -func TestParseRecordValue(t *testing.T) { - testCases := []struct { - desc string - filename string - expected RecordValue - }{ - { - desc: "simple", - filename: "txt_record.txt", - expected: RecordValue{ - "@": []string{"\"v=spf1 a mx ~all\""}, - "_acme-challenge": []string{"\"TheAcmeChallenge\""}, - "_dmarc": []string{"\"v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf\""}, - "_mta-sts": []string{"\"v=STSv1;id=yyyymmddTHHMMSS;\""}, - "_smtp._tls": []string{"\"v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com\""}, - "selector._domainkey": []string{"\"v=DKIM1;k=rsa;p=Base64Stuff\" \"MoreBase64Stuff\" \"Even++MoreBase64Stuff\" \"YesMoreBase64Stuff\" \"And+Yes+Even+MoreBase64Stuff\" \"Sure++MoreBase64Stuff\" \"LastBase64Stuff\""}, - "selectorecc._domainkey": []string{"\"v=DKIM1;k=ed25519;p=Base64Stuff\""}, - }, - }, - { - desc: "multiple values with the same key", - filename: "txt_record-multiple.txt", - expected: RecordValue{ - "@": []string{"\"v=spf1 a mx ~all\""}, - "_acme-challenge": []string{"\"xxx\"", "\"yyy\""}, - "_dmarc": []string{"\"v=DMARC1;p=reject;sp=reject;adkim=r;aspf=r;pct=100;rua=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;ri=86400;ruf=mailto:someone@in.mailhardener.com,mailto:postmaster@example.tld;fo=1;rf=afrf\""}, - "_mta-sts": []string{"\"v=STSv1;id=yyyymmddTHHMMSS;\""}, - "_smtp._tls": []string{"\"v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com\""}, - "selector._domainkey": []string{"\"v=DKIM1;k=rsa;p=Base64Stuff\" \"MoreBase64Stuff\" \"Even++MoreBase64Stuff\" \"YesMoreBase64Stuff\" \"And+Yes+Even+MoreBase64Stuff\" \"Sure++MoreBase64Stuff\" \"LastBase64Stuff\""}, - "selectorecc._domainkey": []string{"\"v=DKIM1;k=ed25519;p=Base64Stuff\""}, - }, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - file, err := os.ReadFile(filepath.Join("fixtures", test.filename)) - require.NoError(t, err) - - data := ParseRecordValue(string(file)) - - assert.Equal(t, test.expected, data) - }) - } -} - -func Test_parseDomains(t *testing.T) { - file, err := os.ReadFile(filepath.FromSlash("./fixtures/domains.txt")) - require.NoError(t, err) - - domains, err := parseDomains(string(file)) - require.NoError(t, err) - - expected := []string{"example.com", "example.org", "example.net"} - - assert.Equal(t, expected, domains) -} diff --git a/providers/dns/arvancloud/arvancloud.go b/providers/dns/arvancloud/arvancloud.go index ed1d5ff7a..759d1922a 100644 --- a/providers/dns/arvancloud/arvancloud.go +++ b/providers/dns/arvancloud/arvancloud.go @@ -9,13 +9,13 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/arvancloud/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) +const minTTL = 600 + // Environment variables names. const ( envNamespace = "ARVANCLOUD_" @@ -28,10 +28,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const minTTL = 600 - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string @@ -96,8 +92,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, client: client, @@ -167,7 +161,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() - if !ok { return fmt.Errorf("arvancloud: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) } diff --git a/providers/dns/arvancloud/arvancloud.toml b/providers/dns/arvancloud/arvancloud.toml index aa5cafb51..f53eb7299 100644 --- a/providers/dns/arvancloud/arvancloud.toml +++ b/providers/dns/arvancloud/arvancloud.toml @@ -6,17 +6,17 @@ Since = "v3.8.0" Example = ''' ARVANCLOUD_API_KEY="Apikey xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \ -lego --dns arvancloud -d '*.example.com' -d example.com run +lego --email you@example.com --dns arvancloud --domains my.example.org run ''' [Configuration] [Configuration.Credentials] ARVANCLOUD_API_KEY = "API key" [Configuration.Additional] - ARVANCLOUD_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - ARVANCLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - ARVANCLOUD_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)" - ARVANCLOUD_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + ARVANCLOUD_POLLING_INTERVAL = "Time between DNS propagation check" + ARVANCLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + ARVANCLOUD_TTL = "The TTL of the TXT record used for the DNS challenge" + ARVANCLOUD_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.arvancloud.ir/docs/api/cdn/4.0" diff --git a/providers/dns/arvancloud/arvancloud_test.go b/providers/dns/arvancloud/arvancloud_test.go index 24013c437..c31edf021 100644 --- a/providers/dns/arvancloud/arvancloud_test.go +++ b/providers/dns/arvancloud/arvancloud_test.go @@ -37,7 +37,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -105,7 +104,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -119,7 +117,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/arvancloud/internal/client.go b/providers/dns/arvancloud/internal/client.go index b447d97c4..3caff392a 100644 --- a/providers/dns/arvancloud/internal/client.go +++ b/providers/dns/arvancloud/internal/client.go @@ -70,7 +70,6 @@ func (c *Client) getRecords(ctx context.Context, domain, search string) ([]DNSRe } response := &apiResponse[[]DNSRecord]{} - err = c.do(req, http.StatusOK, response) if err != nil { return nil, fmt.Errorf("could not get records %s: Domain: %s: %w", search, domain, err) @@ -90,7 +89,6 @@ func (c *Client) CreateRecord(ctx context.Context, domain string, record DNSReco } response := &apiResponse[*DNSRecord]{} - err = c.do(req, http.StatusCreated, response) if err != nil { return nil, fmt.Errorf("could not create record; Domain: %s: %w", domain, err) diff --git a/providers/dns/arvancloud/internal/client_test.go b/providers/dns/arvancloud/internal/client_test.go index 183a8acfd..5c9154c62 100644 --- a/providers/dns/arvancloud/internal/client_test.go +++ b/providers/dns/arvancloud/internal/client_test.go @@ -1,55 +1,103 @@ package internal import ( + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder(apiKey string) *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient(apiKey) - client.baseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() +func setupTest(t *testing.T, apiKey string) (*Client, *http.ServeMux) { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization(apiKey)) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient(apiKey) + client.baseURL, _ = url.Parse(server.URL) + client.HTTPClient = server.Client() + + return client, mux } func TestClient_GetTxtRecord(t *testing.T) { const apiKey = "myKeyA" + client, mux := setupTest(t, apiKey) + const domain = "example.com" - client := mockBuilder(apiKey). - Route("GET /cdn/4.0/domains/"+domain+"/dns-records", - servermock.ResponseFromFixture("get_txt_record.json"), - servermock.CheckQueryParameter().With("search", "acme-challenge")). - Build(t) + mux.HandleFunc("/cdn/4.0/domains/"+domain+"/dns-records", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } - _, err := client.GetTxtRecord(t.Context(), domain, "_acme-challenge", "txtxtxt") + auth := req.Header.Get(authorizationHeader) + if auth != apiKey { + http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized) + return + } + + file, err := os.Open("./fixtures/get_txt_record.json") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + _, err := client.GetTxtRecord(context.Background(), domain, "_acme-challenge", "txtxtxt") require.NoError(t, err) } func TestClient_CreateRecord(t *testing.T) { const apiKey = "myKeyB" + client, mux := setupTest(t, apiKey) + const domain = "example.com" - client := mockBuilder(apiKey). - Route("POST /cdn/4.0/domains/"+domain+"/dns-records", - servermock.ResponseFromFixture("create_txt_record.json"). - WithStatusCode(http.StatusCreated), - servermock.CheckRequestJSONBodyFromFixture("create_record-request.json")). - Build(t) + mux.HandleFunc("/cdn/4.0/domains/"+domain+"/dns-records", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + auth := req.Header.Get(authorizationHeader) + if auth != apiKey { + http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized) + return + } + + file, err := os.Open("./fixtures/create_txt_record.json") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + rw.WriteHeader(http.StatusCreated) + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) record := DNSRecord{ Name: "_acme-challenge", @@ -58,13 +106,13 @@ func TestClient_CreateRecord(t *testing.T) { TTL: 600, } - newRecord, err := client.CreateRecord(t.Context(), domain, record) + newRecord, err := client.CreateRecord(context.Background(), domain, record) require.NoError(t, err) expected := &DNSRecord{ ID: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", Type: "txt", - Value: map[string]any{"text": "txtxtxt"}, + Value: map[string]interface{}{"text": "txtxtxt"}, Name: "_acme-challenge", TTL: 120, UpstreamHTTPS: "default", @@ -81,15 +129,24 @@ func TestClient_CreateRecord(t *testing.T) { func TestClient_DeleteRecord(t *testing.T) { const apiKey = "myKeyC" - const ( - domain = "example.com" - recordID = "recordId" - ) + client, mux := setupTest(t, apiKey) - client := mockBuilder(apiKey). - Route("DELETE /cdn/4.0/domains/"+domain+"/dns-records/"+recordID, nil). - Build(t) + const domain = "example.com" + const recordID = "recordId" - err := client.DeleteRecord(t.Context(), domain, recordID) + mux.HandleFunc("/cdn/4.0/domains/"+domain+"/dns-records/"+recordID, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodDelete { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + auth := req.Header.Get(authorizationHeader) + if auth != apiKey { + http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized) + return + } + }) + + err := client.DeleteRecord(context.Background(), domain, recordID) require.NoError(t, err) } diff --git a/providers/dns/arvancloud/internal/fixtures/create_record-request.json b/providers/dns/arvancloud/internal/fixtures/create_record-request.json deleted file mode 100644 index 48a7124f6..000000000 --- a/providers/dns/arvancloud/internal/fixtures/create_record-request.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "type": "txt", - "value": { - "text": "txtxtxt" - }, - "name": "_acme-challenge", - "ttl": 600 -} diff --git a/providers/dns/auroradns/auroradns.go b/providers/dns/auroradns/auroradns.go index 50d2fbc25..2f759d4a3 100644 --- a/providers/dns/auroradns/auroradns.go +++ b/providers/dns/auroradns/auroradns.go @@ -7,14 +7,13 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/miekg/dns" "github.com/nrdcg/auroradns" ) +const defaultBaseURL = "https://api.auroradns.eu" + // Environment variables names. const ( envNamespace = "AURORA_" @@ -28,10 +27,6 @@ const ( EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) -const defaultBaseURL = "https://api.auroradns.eu" - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string @@ -53,11 +48,10 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - config *Config - client *auroradns.Client - recordIDs map[string]string recordIDsMu sync.Mutex + config *Config + client *auroradns.Client } // NewDNSProvider returns a DNSProvider instance configured for AuroraDNS. @@ -96,7 +90,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, fmt.Errorf("aurora: %w", err) } - client, err := auroradns.NewClient(clientdebug.Wrap(tr.Client()), auroradns.WithBaseURL(config.BaseURL)) + client, err := auroradns.NewClient(tr.Client(), auroradns.WithBaseURL(config.BaseURL)) if err != nil { return nil, fmt.Errorf("aurora: %w", err) } @@ -164,7 +158,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("aurora: unknown recordID for %q", info.EffectiveFQDN) } - authZone, err := dns01.FindZoneByFqdn(dns.Fqdn(info.EffectiveFQDN)) + authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(info.EffectiveFQDN)) if err != nil { return fmt.Errorf("aurora: could not find zone for domain %q: %w", domain, err) } diff --git a/providers/dns/auroradns/auroradns.toml b/providers/dns/auroradns/auroradns.toml index 59b5e7ab1..4afaf7184 100644 --- a/providers/dns/auroradns/auroradns.toml +++ b/providers/dns/auroradns/auroradns.toml @@ -7,7 +7,7 @@ Since = "v0.4.0" Example = ''' AURORA_API_KEY=xxxxx \ AURORA_SECRET=yyyyyy \ -lego --dns auroradns -d '*.example.com' -d example.com run +lego --email you@example.com --dns auroradns --domains my.example.org run ''' [Configuration] @@ -16,9 +16,9 @@ lego --dns auroradns -d '*.example.com' -d example.com run AURORA_SECRET = "Secret password to be used" [Configuration.Additional] AURORA_ENDPOINT = "API endpoint URL" - AURORA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - AURORA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - AURORA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" + AURORA_POLLING_INTERVAL = "Time between DNS propagation check" + AURORA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + AURORA_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://libcloud.readthedocs.io/en/latest/dns/drivers/auroradns.html#api-docs" diff --git a/providers/dns/auroradns/auroradns_test.go b/providers/dns/auroradns/auroradns_test.go index 8a9835d9c..6e4aa54a7 100644 --- a/providers/dns/auroradns/auroradns_test.go +++ b/providers/dns/auroradns/auroradns_test.go @@ -1,32 +1,35 @@ package auroradns import ( + "fmt" + "io" "net/http" "net/http/httptest" "testing" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/nrdcg/auroradns" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var envTest = tester.NewEnvTest(EnvAPIKey, EnvSecret) -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIKey = "asdf1234" - config.Secret = "key" - config.BaseURL = server.URL +func setupTest(t *testing.T) (*DNSProvider, *http.ServeMux) { + t.Helper() - return NewDNSProviderConfig(config) - }, - servermock.CheckHeader(). - WithContentType("application/json"). - WithRegexp("Authorization", `AuroraDNSv1 .+`). - WithRegexp("X-Auroradns-Date", `[0-9TZ]+`)) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + config := NewDefaultConfig() + config.APIKey = "asdf1234" + config.Secret = "key" + config.BaseURL = server.URL + + provider, err := NewDNSProviderConfig(config) + require.NoError(t, err) + + return provider, mux } func TestNewDNSProvider(t *testing.T) { @@ -71,7 +74,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -143,51 +145,76 @@ func TestNewDNSProviderConfig(t *testing.T) { } func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("GET /zones", - servermock.JSONEncode([]auroradns.Zone{{ - ID: "c56a4180-65aa-42ec-a945-5fd21dec0538", - Name: "example.com", - }}). - WithStatusCode(http.StatusCreated)). - Route("POST /zones/c56a4180-65aa-42ec-a945-5fd21dec0538/records", - servermock.JSONEncode(auroradns.Record{ - ID: "ec56a4180-65aa-42ec-a945-5fd21dec0538", - RecordType: "TXT", - Name: "_acme-challenge", - TTL: 300, - }). - WithStatusCode(http.StatusCreated)). - Build(t) + provider, mux := setupTest(t) + + mux.HandleFunc("/zones", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method, "method") + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `[{ + "id": "c56a4180-65aa-42ec-a945-5fd21dec0538", + "name": "example.com" + }]`) + }) + + mux.HandleFunc("/zones/c56a4180-65aa-42ec-a945-5fd21dec0538/records", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "application/json", r.Header.Get("Content-Type"), "Content-Type") + + reqBody, err := io.ReadAll(r.Body) + require.NoError(t, err) + assert.Equal(t, `{"type":"TXT","name":"_acme-challenge","content":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","ttl":300}`, string(reqBody)) + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ + "id": "c56a4180-65aa-42ec-a945-5fd21dec0538", + "type": "TXT", + "name": "_acme-challenge", + "ttl": 300 + }`) + }) err := provider.Present("example.com", "", "foobar") - require.NoError(t, err) + require.NoError(t, err, "fail to create TXT record") } func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("GET /zones", - servermock.JSONEncode([]auroradns.Zone{{ - ID: "c56a4180-65aa-42ec-a945-5fd21dec0538", - Name: "example.com", - }}). - WithStatusCode(http.StatusCreated)). - Route("POST /zones/c56a4180-65aa-42ec-a945-5fd21dec0538/records", - servermock.JSONEncode(auroradns.Record{ - ID: "ec56a4180-65aa-42ec-a945-5fd21dec0538", - RecordType: "TXT", - Name: "_acme-challenge", - TTL: 300, - }). - WithStatusCode(http.StatusCreated)). - Route("DELETE /zones/c56a4180-65aa-42ec-a945-5fd21dec0538/records/ec56a4180-65aa-42ec-a945-5fd21dec0538", - servermock.RawStringResponse("{}"). - WithStatusCode(http.StatusCreated)). - Build(t) + provider, mux := setupTest(t) + + mux.HandleFunc("/zones", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `[{ + "id": "c56a4180-65aa-42ec-a945-5fd21dec0538", + "name": "example.com" + }]`) + }) + + mux.HandleFunc("/zones/c56a4180-65aa-42ec-a945-5fd21dec0538/records", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ + "id": "ec56a4180-65aa-42ec-a945-5fd21dec0538", + "type": "TXT", + "name": "_acme-challenge", + "ttl": 300 + }`) + }) + + mux.HandleFunc("/zones/c56a4180-65aa-42ec-a945-5fd21dec0538/records/ec56a4180-65aa-42ec-a945-5fd21dec0538", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodDelete, r.Method) + + assert.Equal(t, "application/json", r.Header.Get("Content-Type"), "Content-Type") + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{}`) + }) err := provider.Present("example.com", "", "foobar") - require.NoError(t, err) + require.NoError(t, err, "fail to create TXT record") err = provider.CleanUp("example.com", "", "foobar") - require.NoError(t, err) + require.NoError(t, err, "fail to remove TXT record") } diff --git a/providers/dns/autodns/autodns.go b/providers/dns/autodns/autodns.go index 8a9361bc0..67b98d177 100644 --- a/providers/dns/autodns/autodns.go +++ b/providers/dns/autodns/autodns.go @@ -9,11 +9,9 @@ import ( "net/url" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/autodns/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -31,8 +29,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Endpoint *url.URL @@ -106,8 +102,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{config: config, client: client}, nil } @@ -128,9 +122,9 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { Value: info.Value, }} - _, err := d.client.AddRecords(context.Background(), info.EffectiveFQDN, records) + _, err := d.client.AddTxtRecords(context.Background(), info.EffectiveFQDN, records) if err != nil { - return fmt.Errorf("autodns: add record: %w", err) + return fmt.Errorf("autodns: %w", err) } return nil @@ -147,9 +141,8 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { Value: info.Value, }} - _, err := d.client.RemoveRecords(context.Background(), info.EffectiveFQDN, records) - if err != nil { - return fmt.Errorf("autodns: remove record: %w", err) + if err := d.client.RemoveTXTRecords(context.Background(), info.EffectiveFQDN, records); err != nil { + return fmt.Errorf("autodns: %w", err) } return nil diff --git a/providers/dns/autodns/autodns.toml b/providers/dns/autodns/autodns.toml index 2798d4cee..112ec86e3 100644 --- a/providers/dns/autodns/autodns.toml +++ b/providers/dns/autodns/autodns.toml @@ -7,7 +7,7 @@ Since = "v3.2.0" Example = ''' AUTODNS_API_USER=username \ AUTODNS_API_PASSWORD=supersecretpassword \ -lego --dns autodns -d '*.example.com' -d example.com run +lego --email you@example.com --dns autodns --domains my.example.org run ''' [Configuration] @@ -17,10 +17,10 @@ lego --dns autodns -d '*.example.com' -d example.com run [Configuration.Additional] AUTODNS_ENDPOINT = "API endpoint URL, defaults to https://api.autodns.com/v1/" AUTODNS_CONTEXT = "API context (4 for production, 1 for testing. Defaults to 4)" - AUTODNS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)" - AUTODNS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - AUTODNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - AUTODNS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + AUTODNS_TTL = "The TTL of the TXT record used for the DNS challenge" + AUTODNS_POLLING_INTERVAL = "Time between DNS propagation check" + AUTODNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + AUTODNS_HTTP_TIMEOUT = "API request timeout, defaults to 30 seconds" [Links] API = "https://help.internetx.com/display/APIJSONEN" diff --git a/providers/dns/autodns/autodns_test.go b/providers/dns/autodns/autodns_test.go index 632d24705..bc9f3067e 100644 --- a/providers/dns/autodns/autodns_test.go +++ b/providers/dns/autodns/autodns_test.go @@ -57,7 +57,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -132,7 +131,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -146,7 +144,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/autodns/internal/client.go b/providers/dns/autodns/internal/client.go index d92490a60..363250d0a 100644 --- a/providers/dns/autodns/internal/client.go +++ b/providers/dns/autodns/internal/client.go @@ -31,7 +31,7 @@ type Client struct { } // NewClient creates a new Client. -func NewClient(username, password string, clientContext int) *Client { +func NewClient(username string, password string, clientContext int) *Client { baseURL, _ := url.Parse(DefaultEndpoint) return &Client{ @@ -43,22 +43,23 @@ func NewClient(username, password string, clientContext int) *Client { } } -// AddRecords adds records. -func (c *Client) AddRecords(ctx context.Context, domain string, records []*ResourceRecord) (*DataZoneResponse, error) { +// AddTxtRecords adds TXT records. +func (c *Client) AddTxtRecords(ctx context.Context, domain string, records []*ResourceRecord) (*Zone, error) { zoneStream := &ZoneStream{Adds: records} return c.updateZone(ctx, domain, zoneStream) } -// RemoveRecords removes records. -func (c *Client) RemoveRecords(ctx context.Context, domain string, records []*ResourceRecord) (*DataZoneResponse, error) { +// RemoveTXTRecords removes TXT records. +func (c *Client) RemoveTXTRecords(ctx context.Context, domain string, records []*ResourceRecord) error { zoneStream := &ZoneStream{Removes: records} - return c.updateZone(ctx, domain, zoneStream) + _, err := c.updateZone(ctx, domain, zoneStream) + return err } // https://github.com/InterNetX/domainrobot-api/blob/bdc8fe92a2f32fcbdb29e30bf6006ab446f81223/src/domainrobot.json#L21090 -func (c *Client) updateZone(ctx context.Context, domain string, zoneStream *ZoneStream) (*DataZoneResponse, error) { +func (c *Client) updateZone(ctx context.Context, domain string, zoneStream *ZoneStream) (*Zone, error) { endpoint := c.BaseURL.JoinPath("zone", domain, "_stream") req, err := newJSONRequest(ctx, http.MethodPost, endpoint, zoneStream) @@ -66,12 +67,12 @@ func (c *Client) updateZone(ctx context.Context, domain string, zoneStream *Zone return nil, err } - var resp *DataZoneResponse - if err := c.do(req, &resp); err != nil { + var zone *Zone + if err := c.do(req, &zone); err != nil { return nil, err } - return resp, nil + return zone, nil } func (c *Client) do(req *http.Request, result any) error { @@ -86,7 +87,7 @@ func (c *Client) do(req *http.Request, result any) error { defer func() { _ = resp.Body.Close() }() if resp.StatusCode/100 != 2 { - return parseError(req, resp) + return errutils.NewUnexpectedResponseStatusCodeError(req, resp) } if result == nil { @@ -129,16 +130,3 @@ func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, paylo return req, nil } - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIError - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return &errAPI -} diff --git a/providers/dns/autodns/internal/client_test.go b/providers/dns/autodns/internal/client_test.go index 9b0968fdc..f8743b24b 100644 --- a/providers/dns/autodns/internal/client_test.go +++ b/providers/dns/autodns/internal/client_test.go @@ -1,174 +1,96 @@ package internal import ( + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("user", "secret", 123) - client.HTTPClient = server.Client() - client.BaseURL, _ = url.Parse(server.URL) +func setupTest(t *testing.T, method, pattern string, status int, file string) *Client { + t.Helper() - return client, nil - }, - servermock.CheckHeader(). - WithBasicAuth("user", "secret"). - WithJSONHeaders()) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest) + return + } + + apiUser, apiKey, ok := req.BasicAuth() + if apiUser != "user" || apiKey != "secret" || !ok { + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + if file == "" { + rw.WriteHeader(status) + return + } + + open, err := os.Open(filepath.Join("fixtures", file)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = open.Close() }() + + rw.WriteHeader(status) + _, err = io.Copy(rw, open) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + client := NewClient("user", "secret", 123) + client.HTTPClient = server.Client() + client.BaseURL, _ = url.Parse(server.URL) + + return client } -func TestClient_AddRecords(t *testing.T) { - client := mockBuilder(). - Route("POST /zone/example.com/_stream", - servermock.ResponseFromFixture("add_record.json"), - servermock.CheckRequestJSONBodyFromFixture("add_record-request.json"), - servermock.CheckHeader(). - With("X-Domainrobot-Context", "123")). - Build(t) +func TestClient_AddTxtRecords(t *testing.T) { + client := setupTest(t, http.MethodPost, "/zone/example.com/_stream", http.StatusOK, "add-record.json") - records := []*ResourceRecord{{ - Name: "example.com", - TTL: 600, - Type: "TXT", - Value: "txtTXTtxt", - }} + records := []*ResourceRecord{{}} - resp, err := client.AddRecords(t.Context(), "example.com", records) + zone, err := client.AddTxtRecords(context.Background(), "example.com", records) require.NoError(t, err) - expected := &DataZoneResponse{ - STID: "20251121-appf4923-126284", - CTID: "", - Messages: []ResponseMessage{ - { - Text: "string", - Messages: []string{ - "string", - }, - Objects: []GenericObject{ - { - Type: "string", - Value: "string", - }, - }, - Code: "string", - Status: "SUCCESS", - }, - }, - Status: &ResponseStatus{ - Code: "S0301", - Text: "Zone was updated successfully on the name server.", - Type: "SUCCESS", - }, - Object: nil, - Data: []Zone{ - { - Name: "example.com", - ResourceRecords: []ResourceRecord{ - { - Name: "example.com", - TTL: 120, - Type: "TXT", - Value: "txt", - Pref: 1, - }, - }, - Action: "xxx", - VirtualNameServer: "yyy", - }, - }, + expected := &Zone{ + Name: "example.com", + ResourceRecords: []*ResourceRecord{{ + Name: "example.com", + TTL: 120, + Type: "TXT", + Value: "txt", + Pref: 1, + }}, + Action: "xxx", + VirtualNameServer: "yyy", } - assert.Equal(t, expected, resp) + assert.Equal(t, expected, zone) } -func TestClient_AddRecords_error(t *testing.T) { - client := mockBuilder(). - Route("POST /zone/example.com/_stream", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) +func TestClient_RemoveTXTRecords(t *testing.T) { + client := setupTest(t, http.MethodPost, "/zone/example.com/_stream", http.StatusOK, "add-record.json") - records := []*ResourceRecord{{ - Name: "example.com", - TTL: 600, - Type: "TXT", - Value: "txtTXTtxt", - }} + records := []*ResourceRecord{{}} - _, err := client.AddRecords(t.Context(), "example.com", records) - require.EqualError(t, err, `STID: 20251121-appf4923-126284, status: code: E0202002, text: Zone konnte auf dem Nameserver nicht aktualisiert werden., type: ERROR, message: code: EF02022, text: Der Zusatzeintrag wurde doppelt eingetragen., status: ERROR, object: OURDOMAIN.TLD@nsa7.schlundtech.de/rr[17]: _acme-challenge.www.whoami.int.OURDOMAIN.TLD TXT "rK2SJb_ZcrYefbfCKU6jZEANfEAJeOtSh1Fv8hkUoVc"`) -} - -func TestClient_RemoveRecords(t *testing.T) { - client := mockBuilder(). - Route("POST /zone/example.com/_stream", - servermock.ResponseFromFixture("remove_record.json"), - servermock.CheckRequestJSONBodyFromFixture("remove_record-request.json"), - servermock.CheckHeader(). - With("X-Domainrobot-Context", "123")). - Build(t) - - records := []*ResourceRecord{{ - Name: "example.com", - TTL: 600, - Type: "TXT", - Value: "txtTXTtxt", - }} - - resp, err := client.RemoveRecords(t.Context(), "example.com", records) + err := client.RemoveTXTRecords(context.Background(), "example.com", records) require.NoError(t, err) - - expected := &DataZoneResponse{ - STID: "20251121-appf4923-126284", - CTID: "", - Messages: []ResponseMessage{ - { - Text: "string", - Messages: []string{ - "string", - }, - Objects: []GenericObject{ - { - Type: "string", - Value: "string", - }, - }, - Code: "string", - Status: "SUCCESS", - }, - }, - Status: &ResponseStatus{ - Code: "S0301", - Text: "Zone was updated successfully on the name server.", - Type: "SUCCESS", - }, - Object: nil, - Data: []Zone{ - { - Name: "example.com", - ResourceRecords: []ResourceRecord{ - { - Name: "example.com", - TTL: 120, - Type: "TXT", - Value: "txt", - Pref: 1, - }, - }, - Action: "xxx", - VirtualNameServer: "yyy", - }, - }, - } - - assert.Equal(t, expected, resp) } diff --git a/providers/dns/autodns/internal/fixtures/add-record.json b/providers/dns/autodns/internal/fixtures/add-record.json new file mode 100644 index 000000000..4a95f0784 --- /dev/null +++ b/providers/dns/autodns/internal/fixtures/add-record.json @@ -0,0 +1,14 @@ +{ + "origin": "example.com", + "resourceRecords": [ + { + "name": "example.com", + "ttl": 120, + "type": "TXT", + "value": "txt", + "pref": 1 + } + ], + "action": "xxx", + "virtualNameServer": "yyy" +} diff --git a/providers/dns/autodns/internal/fixtures/add_record-request.json b/providers/dns/autodns/internal/fixtures/add_record-request.json deleted file mode 100644 index 6105c77ac..000000000 --- a/providers/dns/autodns/internal/fixtures/add_record-request.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "adds": [ - { - "name": "example.com", - "ttl": 600, - "type": "TXT", - "value": "txtTXTtxt" - } - ], - "rems": null -} diff --git a/providers/dns/autodns/internal/fixtures/add_record.json b/providers/dns/autodns/internal/fixtures/add_record.json deleted file mode 100644 index a0ce66ba6..000000000 --- a/providers/dns/autodns/internal/fixtures/add_record.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "stid": "20251121-appf4923-126284", - "messages": [ - { - "text": "string", - "notice": "string", - "messages": [ - "string" - ], - "objects": [ - { - "type": "string", - "value": "string" - } - ], - "code": "string", - "status": "SUCCESS" - } - ], - "status": { - "code": "S0301", - "text": "Zone was updated successfully on the name server.", - "type": "SUCCESS" - }, - "data": [ - { - "origin": "example.com", - "resourceRecords": [ - { - "name": "example.com", - "ttl": 120, - "type": "TXT", - "value": "txt", - "pref": 1 - } - ], - "action": "xxx", - "virtualNameServer": "yyy" - } - ] -} diff --git a/providers/dns/autodns/internal/fixtures/error.json b/providers/dns/autodns/internal/fixtures/error.json deleted file mode 100644 index 2ed635d58..000000000 --- a/providers/dns/autodns/internal/fixtures/error.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "stid": "20251121-appf4923-126284", - "messages": [ - { - "text": "Der Zusatzeintrag wurde doppelt eingetragen.", - "objects": [ - { - "type": "OURDOMAIN.TLD@nsa7.schlundtech.de/rr[17]", - "value": "_acme-challenge.www.whoami.int.OURDOMAIN.TLD TXT \"rK2SJb_ZcrYefbfCKU6jZEANfEAJeOtSh1Fv8hkUoVc\"" - } - ], - "code": "EF02022", - "status": "ERROR" - } - ], - "status": { - "code": "E0202002", - "text": "Zone konnte auf dem Nameserver nicht aktualisiert werden.", - "type": "ERROR" - } -} diff --git a/providers/dns/autodns/internal/fixtures/remove-record.json b/providers/dns/autodns/internal/fixtures/remove-record.json new file mode 100644 index 000000000..4a95f0784 --- /dev/null +++ b/providers/dns/autodns/internal/fixtures/remove-record.json @@ -0,0 +1,14 @@ +{ + "origin": "example.com", + "resourceRecords": [ + { + "name": "example.com", + "ttl": 120, + "type": "TXT", + "value": "txt", + "pref": 1 + } + ], + "action": "xxx", + "virtualNameServer": "yyy" +} diff --git a/providers/dns/autodns/internal/fixtures/remove_record-request.json b/providers/dns/autodns/internal/fixtures/remove_record-request.json deleted file mode 100644 index 92361403e..000000000 --- a/providers/dns/autodns/internal/fixtures/remove_record-request.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "adds": null, - "rems": [ - { - "name": "example.com", - "ttl": 600, - "type": "TXT", - "value": "txtTXTtxt" - } - ] -} diff --git a/providers/dns/autodns/internal/fixtures/remove_record.json b/providers/dns/autodns/internal/fixtures/remove_record.json deleted file mode 100644 index a0ce66ba6..000000000 --- a/providers/dns/autodns/internal/fixtures/remove_record.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "stid": "20251121-appf4923-126284", - "messages": [ - { - "text": "string", - "notice": "string", - "messages": [ - "string" - ], - "objects": [ - { - "type": "string", - "value": "string" - } - ], - "code": "string", - "status": "SUCCESS" - } - ], - "status": { - "code": "S0301", - "text": "Zone was updated successfully on the name server.", - "type": "SUCCESS" - }, - "data": [ - { - "origin": "example.com", - "resourceRecords": [ - { - "name": "example.com", - "ttl": 120, - "type": "TXT", - "value": "txt", - "pref": 1 - } - ], - "action": "xxx", - "virtualNameServer": "yyy" - } - ] -} diff --git a/providers/dns/autodns/internal/types.go b/providers/dns/autodns/internal/types.go index 8a06f4889..93fd678ca 100644 --- a/providers/dns/autodns/internal/types.go +++ b/providers/dns/autodns/internal/types.go @@ -1,133 +1,33 @@ package internal -import ( - "fmt" - "strings" -) - -type APIResponse[T any] struct { - STID string `json:"stid"` - CTID string `json:"ctid"` - Messages []ResponseMessage `json:"messages"` - Status *ResponseStatus `json:"status"` - Object *ResponseObject `json:"object"` - Data T `json:"data"` -} - -type APIError APIResponse[any] - -func (a *APIError) Error() string { - var parts []string - - if a.STID != "" { - parts = append(parts, fmt.Sprintf("STID: %s", a.STID)) - } - - if a.CTID != "" { - parts = append(parts, fmt.Sprintf("CTID: %s", a.CTID)) - } - - if a.Status != nil { - parts = append(parts, "status: "+a.Status.String()) - } - - for _, message := range a.Messages { - parts = append(parts, "message: "+message.String()) - } - - if a.Object != nil { - parts = append(parts, "object: "+a.Object.String()) - } - - return strings.Join(parts, ", ") -} - -type DataZoneResponse APIResponse[[]Zone] - type ResponseMessage struct { - Text string `json:"text"` - Code string `json:"code"` - Status string `json:"status"` - Messages []string `json:"messages"` - Objects []GenericObject `json:"objects"` -} - -func (r ResponseMessage) String() string { - var parts []string - - if r.Code != "" { - parts = append(parts, "code: "+r.Code) - } - - if r.Text != "" { - parts = append(parts, "text: "+r.Text) - } - - if r.Status != "" { - parts = append(parts, "status: "+r.Status) - } - - if len(r.Messages) > 0 { - parts = append(parts, "messages: "+strings.Join(r.Messages, ";")) - } - - for _, object := range r.Objects { - parts = append(parts, fmt.Sprintf("object: %s", object)) - } - - return strings.Join(parts, ", ") -} - -type GenericObject struct { - Type string `json:"type"` - Value string `json:"value"` -} - -func (g GenericObject) String() string { - return g.Type + ": " + g.Value + Text string `json:"text"` + Messages []string `json:"messages"` + Objects []string `json:"objects"` + Code string `json:"code"` + Status string `json:"status"` } type ResponseStatus struct { Code string `json:"code"` Text string `json:"text"` - Type string `json:"type"` // SUCCESS, ERROR, NOTIFY, NOTICE, NICCOM_NOTIFY -} - -func (r ResponseStatus) String() string { - return fmt.Sprintf("code: %s, text: %s, type: %s", r.Code, r.Text, r.Type) + Type string `json:"type"` } type ResponseObject struct { - Type string `json:"type"` - Value string `json:"value"` - Summary int32 `json:"summary"` - Data *ResponseObjectData `json:"data"` + Type string `json:"type"` + Value string `json:"value"` + Summary int32 `json:"summary"` + Data string } -func (r ResponseObject) String() string { - var parts []string - - if r.Type != "" { - parts = append(parts, fmt.Sprintf("type: %s", r.Type)) - } - - if r.Value != "" { - parts = append(parts, fmt.Sprintf("value: %s", r.Value)) - } - - if r.Summary != 0 { - parts = append(parts, fmt.Sprintf("summary: %d", r.Summary)) - } - - if r.Data != nil { - parts = append(parts, fmt.Sprintf("data: %s", r.Data.Description)) - } - - return strings.Join(parts, ", ") -} - -type ResponseObjectData struct { - Description string `json:"description"` +type DataZoneResponse struct { + STID string `json:"stid"` + CTID string `json:"ctid"` + Messages []*ResponseMessage `json:"messages"` + Status *ResponseStatus `json:"status"` + Object any `json:"object"` + Data []*Zone `json:"data"` } // ResourceRecord holds a resource record. @@ -143,10 +43,10 @@ type ResourceRecord struct { // Zone is an autodns zone record with all for us relevant fields. // https://help.internetx.com/display/APIXMLEN/Zone+Object type Zone struct { - Name string `json:"origin"` - ResourceRecords []ResourceRecord `json:"resourceRecords"` - Action string `json:"action"` - VirtualNameServer string `json:"virtualNameServer"` + Name string `json:"origin"` + ResourceRecords []*ResourceRecord `json:"resourceRecords"` + Action string `json:"action"` + VirtualNameServer string `json:"virtualNameServer"` } // ZoneStream body of the requests. diff --git a/providers/dns/axelname/axelname.go b/providers/dns/axelname/axelname.go deleted file mode 100644 index 96d26236e..000000000 --- a/providers/dns/axelname/axelname.go +++ /dev/null @@ -1,160 +0,0 @@ -// Package axelname implements a DNS provider for solving the DNS-01 challenge using Axelname. -package axelname - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/axelname/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "AXELNAME_" - - EnvNickname = envNamespace + "NICKNAME" - EnvToken = envNamespace + "TOKEN" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - Nickname string - Token string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for Axelname. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvNickname, EnvToken) - if err != nil { - return nil, fmt.Errorf("axelname: %w", err) - } - - config := NewDefaultConfig() - config.Nickname = values[EnvNickname] - config.Token = values[EnvToken] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Axelname. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("axelname: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.Nickname, config.Token) - if err != nil { - return nil, fmt.Errorf("axelname: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("axelname: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("axelname: %w", err) - } - - record := internal.Record{ - Name: subDomain, - Type: "TXT", - Value: info.Value, - } - - err = d.client.AddRecord(context.Background(), dns01.UnFqdn(authZone), record) - if err != nil { - return fmt.Errorf("axelname: add record: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("axelname: could not find zone for domain %q: %w", domain, err) - } - - records, err := d.client.ListRecords(ctx, dns01.UnFqdn(authZone)) - if err != nil { - return fmt.Errorf("axelname: list records: %w", err) - } - - for _, record := range records { - if record.Type != "TXT" || record.Value != info.Value { - continue - } - - err = d.client.DeleteRecord(ctx, dns01.UnFqdn(authZone), record) - if err != nil { - return fmt.Errorf("axelname: delete record: %w", err) - } - - return nil - } - - return errors.New("axelname: delete record: record not found") -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/axelname/axelname.toml b/providers/dns/axelname/axelname.toml deleted file mode 100644 index 1e2ad6e72..000000000 --- a/providers/dns/axelname/axelname.toml +++ /dev/null @@ -1,24 +0,0 @@ -Name = "Axelname" -Description = '''''' -URL = "https://axelname.ru" -Code = "axelname" -Since = "v4.23.0" - -Example = ''' -AXELNAME_NICKNAME="yyy" \ -AXELNAME_TOKEN="xxx" \ -lego --dns axelname -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - AXELNAME_NICKNAME = "Account nickname" - AXELNAME_TOKEN = "API token" - [Configuration.Additional] - AXELNAME_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - AXELNAME_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - AXELNAME_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - AXELNAME_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://axelname.ru/static/content/files/axelname_api_rest_lite.pdf" diff --git a/providers/dns/axelname/axelname_test.go b/providers/dns/axelname/axelname_test.go deleted file mode 100644 index 1a8bac971..000000000 --- a/providers/dns/axelname/axelname_test.go +++ /dev/null @@ -1,144 +0,0 @@ -package axelname - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvNickname, EnvToken).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvNickname: "user", - EnvToken: "secret", - }, - }, - { - desc: "missing nickname", - envVars: map[string]string{ - EnvNickname: "", - EnvToken: "secret", - }, - expected: "axelname: some credentials information are missing: AXELNAME_NICKNAME", - }, - { - desc: "missing token", - envVars: map[string]string{ - EnvNickname: "user", - EnvToken: "", - }, - expected: "axelname: some credentials information are missing: AXELNAME_TOKEN", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "axelname: some credentials information are missing: AXELNAME_NICKNAME,AXELNAME_TOKEN", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - token string - nickname string - expected string - }{ - { - desc: "success", - nickname: "user", - token: "secret", - }, - { - desc: "missing nickname", - expected: "axelname: credentials missing", - }, - { - desc: "missing token", - expected: "axelname: credentials missing", - }, - { - desc: "missing credentials", - expected: "axelname: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.Token = test.token - config.Nickname = test.nickname - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/axelname/internal/client.go b/providers/dns/axelname/internal/client.go deleted file mode 100644 index f2defec87..000000000 --- a/providers/dns/axelname/internal/client.go +++ /dev/null @@ -1,184 +0,0 @@ -package internal - -import ( - "context" - "encoding/json" - "errors" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - querystring "github.com/google/go-querystring/query" -) - -const statusSuccess = "success" - -const defaultBaseURL = "https://my.axelname.ru/rest/" - -// Client the Axelname API client. -type Client struct { - nickname string - token string - - baseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(nickname, token string) (*Client, error) { - if token == "" || nickname == "" { - return nil, errors.New("credentials missing") - } - - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - nickname: nickname, - token: token, - baseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -func (c *Client) ListRecords(ctx context.Context, domain string) ([]Record, error) { - endpoint := c.baseURL.JoinPath("dns_list") - - query := endpoint.Query() - query.Set("domain", domain) - - endpoint.RawQuery = query.Encode() - - req, err := c.newRequest(ctx, endpoint) - if err != nil { - return nil, err - } - - var results ListResponse - - err = c.do(req, &results) - if err != nil { - return nil, err - } - - if results.Result != statusSuccess { - return nil, &results.APIError - } - - return results.List, nil -} - -func (c *Client) DeleteRecord(ctx context.Context, domain string, record Record) error { - endpoint := c.baseURL.JoinPath("dns_delete") - - values, err := querystring.Values(record) - if err != nil { - return err - } - - values.Set("domain", domain) - - endpoint.RawQuery = values.Encode() - - req, err := c.newRequest(ctx, endpoint) - if err != nil { - return err - } - - var results APIResponse - - err = c.do(req, &results) - if err != nil { - return err - } - - if results.Result != statusSuccess { - return &results.APIError - } - - return nil -} - -func (c *Client) AddRecord(ctx context.Context, domain string, record Record) error { - endpoint := c.baseURL.JoinPath("dns_add") - - values, err := querystring.Values(record) - if err != nil { - return err - } - - values.Set("domain", domain) - - endpoint.RawQuery = values.Encode() - - req, err := c.newRequest(ctx, endpoint) - if err != nil { - return err - } - - var results APIResponse - - err = c.do(req, &results) - if err != nil { - return err - } - - if results.Result != statusSuccess { - return &results.APIError - } - - return nil -} - -func (c *Client) newRequest(ctx context.Context, endpoint *url.URL) (*http.Request, error) { - query := endpoint.Query() - query.Set("token", c.token) - query.Set("nichdl", c.nickname) - - endpoint.RawQuery = query.Encode() - - return http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) -} - -func (c *Client) do(req *http.Request, result any) error { - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIError - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return &errAPI -} diff --git a/providers/dns/axelname/internal/client_test.go b/providers/dns/axelname/internal/client_test.go deleted file mode 100644 index 7796f6047..000000000 --- a/providers/dns/axelname/internal/client_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func setupClient(server *httptest.Server) (*Client, error) { - client, err := NewClient("user", "secret") - if err != nil { - return nil, err - } - - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) - - return client, nil -} - -func TestClient_ListRecords(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /dns_list", - servermock.ResponseFromFixture("dns_list.json"), - servermock.CheckQueryParameter().Strict(). - With("domain", "example.com"). - With("nichdl", "user"). - With("token", "secret")). - Build(t) - - records, err := client.ListRecords(t.Context(), "example.com") - require.NoError(t, err) - - expected := []Record{ - {ID: "74749", Name: "example.com", Type: "A", Value: "46.161.54.22"}, - {ID: "417", Name: "example.com", Type: "MX", Value: "mx.yandex.ru.", Prio: "10"}, - {ID: "419", Name: "mail.example.com", Type: "CNAME", Value: "mail.yandex.ru."}, - {ID: "74750", Name: "www.example.com", Type: "A", Value: "46.161.54.22"}, - } - - assert.Equal(t, expected, records) -} - -func TestClient_ListRecords_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /dns_list", - servermock.ResponseFromFixture("dns_list_error.json"). - WithStatusCode(http.StatusNotFound)). - Build(t) - - _, err := client.ListRecords(t.Context(), "example.com") - require.EqualError(t, err, "error: Domain not found (1)") -} - -func TestClient_DeleteRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /dns_delete", - servermock.ResponseFromFixture("dns_delete.json"), - servermock.CheckQueryParameter().Strict(). - With("id", "74749"). - With("domain", "example.com"). - With("nichdl", "user"). - With("token", "secret")). - Build(t) - - record := Record{ID: "74749"} - - err := client.DeleteRecord(t.Context(), "example.com", record) - require.NoError(t, err) -} - -func TestClient_DeleteRecord_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /dns_delete", - servermock.ResponseFromFixture("dns_delete_error.json"). - WithStatusCode(http.StatusNotFound)). - Build(t) - - record := Record{ID: "74749"} - - err := client.DeleteRecord(t.Context(), "example.com", record) - require.EqualError(t, err, "error: Domain not found (1)") -} - -func TestClient_AddRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /dns_add", - servermock.ResponseFromFixture("dns_add.json"), - servermock.CheckQueryParameter().Strict(). - With("id", "74749"). - With("domain", "example.com"). - With("nichdl", "user"). - With("token", "secret")). - Build(t) - - record := Record{ID: "74749"} - - err := client.AddRecord(t.Context(), "example.com", record) - require.NoError(t, err) -} - -func TestClient_AddRecord_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /dns_add", - servermock.ResponseFromFixture("dns_add_error.json"). - WithStatusCode(http.StatusNotFound)). - Build(t) - - record := Record{ID: "74749"} - - err := client.AddRecord(t.Context(), "example.com", record) - require.EqualError(t, err, "error: Domain not found (1)") -} diff --git a/providers/dns/axelname/internal/fixtures/dns_add.json b/providers/dns/axelname/internal/fixtures/dns_add.json deleted file mode 100644 index 628813579..000000000 --- a/providers/dns/axelname/internal/fixtures/dns_add.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "OK", - "message": "DNS record added", - "result": "success" -} diff --git a/providers/dns/axelname/internal/fixtures/dns_add_error.json b/providers/dns/axelname/internal/fixtures/dns_add_error.json deleted file mode 100644 index 5fb9fd368..000000000 --- a/providers/dns/axelname/internal/fixtures/dns_add_error.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "error_code": "1", - "error_text": "Domain not found", - "result": "error" -} diff --git a/providers/dns/axelname/internal/fixtures/dns_delete.json b/providers/dns/axelname/internal/fixtures/dns_delete.json deleted file mode 100644 index a7851fcc6..000000000 --- a/providers/dns/axelname/internal/fixtures/dns_delete.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "OK", - "message": "DNS record deleted", - "result": "success" -} diff --git a/providers/dns/axelname/internal/fixtures/dns_delete_error.json b/providers/dns/axelname/internal/fixtures/dns_delete_error.json deleted file mode 100644 index 5fb9fd368..000000000 --- a/providers/dns/axelname/internal/fixtures/dns_delete_error.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "error_code": "1", - "error_text": "Domain not found", - "result": "error" -} diff --git a/providers/dns/axelname/internal/fixtures/dns_list.json b/providers/dns/axelname/internal/fixtures/dns_list.json deleted file mode 100644 index ace11ba73..000000000 --- a/providers/dns/axelname/internal/fixtures/dns_list.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "code": "OK", - "message": "DNS-records", - "count": 4, - "result": "success", - "list": [ - { - "id": "74749", - "name": "example.com", - "type": "A", - "value": "46.161.54.22" - }, - { - "id": "417", - "name": "example.com", - "type": "MX", - "value": "mx.yandex.ru.", - "prio": "10" - }, - { - "id": "419", - "name": "mail.example.com", - "type": "CNAME", - "value": "mail.yandex.ru." - }, - { - "id": "74750", - "name": "www.example.com", - "type": "A", - "value": "46.161.54.22" - } - ] -} diff --git a/providers/dns/axelname/internal/fixtures/dns_list_error.json b/providers/dns/axelname/internal/fixtures/dns_list_error.json deleted file mode 100644 index 5fb9fd368..000000000 --- a/providers/dns/axelname/internal/fixtures/dns_list_error.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "error_code": "1", - "error_text": "Domain not found", - "result": "error" -} diff --git a/providers/dns/axelname/internal/types.go b/providers/dns/axelname/internal/types.go deleted file mode 100644 index 45583fb2e..000000000 --- a/providers/dns/axelname/internal/types.go +++ /dev/null @@ -1,35 +0,0 @@ -package internal - -import "fmt" - -type APIError struct { - ErrorCode string `json:"error_code,omitempty"` - ErrorText string `json:"error_text,omitempty"` - Result string `json:"result,omitempty"` -} - -func (a *APIError) Error() string { - return fmt.Sprintf("%s: %s (%s)", a.Result, a.ErrorText, a.ErrorCode) -} - -type APIResponse struct { - APIError - - Code string `json:"code,omitempty"` - Message string `json:"message,omitempty"` -} - -type ListResponse struct { - APIResponse - - Count int `json:"count,omitempty"` - List []Record `json:"list,omitempty"` -} - -type Record struct { - ID string `json:"id,omitempty" url:"id,omitempty"` - Name string `json:"name,omitempty" url:"name,omitempty"` - Type string `json:"type,omitempty" url:"type,omitempty"` - Value string `json:"value,omitempty" url:"value,omitempty"` - Prio string `json:"prio,omitempty" url:"prio,omitempty"` -} diff --git a/providers/dns/azion/azion.go b/providers/dns/azion/azion.go deleted file mode 100644 index 5584ece0b..000000000 --- a/providers/dns/azion/azion.go +++ /dev/null @@ -1,307 +0,0 @@ -// Package azion implements a DNS provider for solving the DNS-01 challenge using Azion Edge DNS. -package azion - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/aziontech/azionapi-go-sdk/idns" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "AZION_" - - EnvPersonalToken = envNamespace + "PERSONAL_TOKEN" - EnvPageSize = envNamespace + "PAGE_SIZE" - - EnvTTL = envNamespace + "TTL" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - PersonalToken string - PageSize int - - PollingInterval time.Duration - PropagationTimeout time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - PageSize: env.GetOrDefaultInt(EnvPageSize, 50), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *idns.APIClient -} - -// NewDNSProvider returns a DNSProvider instance configured for Azion. -// Credentials must be passed in the environment variable: AZION_PERSONAL_TOKEN. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvPersonalToken) - if err != nil { - return nil, fmt.Errorf("azion: %w", err) - } - - config := NewDefaultConfig() - config.PersonalToken = values[EnvPersonalToken] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Azion. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("azion: the configuration of the DNS provider is nil") - } - - if config.PersonalToken == "" { - return nil, errors.New("azion: missing credentials") - } - - clientConfig := idns.NewConfiguration() - clientConfig.AddDefaultHeader("Accept", "application/json; version=3") - clientConfig.UserAgent = "lego-dns/azion" - - if config.HTTPClient != nil { - clientConfig.HTTPClient = config.HTTPClient - } - - clientConfig.HTTPClient = clientdebug.Wrap(clientConfig.HTTPClient) - - client := idns.NewAPIClient(clientConfig) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctxAuth := authContext(context.Background(), d.config.PersonalToken) - - zone, err := d.findZone(ctxAuth, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("azion: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := extractSubDomain(info, zone) - if err != nil { - return fmt.Errorf("azion: %w", err) - } - - // Check if a TXT record with the same name already exists - existingRecord, err := d.findExistingTXTRecord(ctxAuth, zone.GetId(), subDomain) - if err != nil { - return fmt.Errorf("azion: check existing records: %w", err) - } - - record := idns.NewRecordPostOrPut() - record.SetEntry(subDomain) - record.SetRecordType("TXT") - record.SetTtl(int32(d.config.TTL)) - - var resp *idns.PostOrPutRecordResponse - - if existingRecord != nil { - // Update existing record by adding the new value to the existing ones - record.SetAnswersList(append(existingRecord.GetAnswersList(), info.Value)) - - // Use PUT to update the existing record - resp, _, err = d.client.RecordsAPI.PutZoneRecord(ctxAuth, zone.GetId(), existingRecord.GetRecordId()).RecordPostOrPut(*record).Execute() - if err != nil { - return fmt.Errorf("azion: update existing record: %w", err) - } - } else { - // Create a new record - record.SetAnswersList([]string{info.Value}) - - resp, _, err = d.client.RecordsAPI.PostZoneRecord(ctxAuth, zone.GetId()).RecordPostOrPut(*record).Execute() - if err != nil { - return fmt.Errorf("azion: create new zone record: %w", err) - } - } - - if resp == nil || resp.Results == nil { - return errors.New("azion: create zone record error") - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctxAuth := authContext(context.Background(), d.config.PersonalToken) - - zone, err := d.findZone(ctxAuth, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("azion: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := extractSubDomain(info, zone) - if err != nil { - return fmt.Errorf("azion: %w", err) - } - - existingRecord, err := d.findExistingTXTRecord(ctxAuth, zone.GetId(), subDomain) - if err != nil { - return fmt.Errorf("azion: find existing record: %w", err) - } - - if existingRecord == nil { - return nil - } - - currentAnswers := existingRecord.GetAnswersList() - - var updatedAnswers []string - - for _, answer := range currentAnswers { - if answer != info.Value { - updatedAnswers = append(updatedAnswers, answer) - } - } - - // If no answers remain, delete the entire record - if len(updatedAnswers) == 0 { - _, resp, errDelete := d.client.RecordsAPI.DeleteZoneRecord(ctxAuth, zone.GetId(), existingRecord.GetRecordId()).Execute() - if errDelete != nil { - // If a record doesn't exist (404), consider cleanup successful - if resp != nil && resp.StatusCode == http.StatusNotFound { - return nil - } - - return fmt.Errorf("azion: delete record: %w", errDelete) - } - - return nil - } - - // Update the record with remaining answers - record := idns.NewRecordPostOrPut() - record.SetEntry(subDomain) - record.SetRecordType("TXT") - record.SetAnswersList(updatedAnswers) - record.SetTtl(existingRecord.GetTtl()) - - _, _, err = d.client.RecordsAPI.PutZoneRecord(ctxAuth, zone.GetId(), existingRecord.GetRecordId()).RecordPostOrPut(*record).Execute() - if err != nil { - return fmt.Errorf("azion: update record: %w", err) - } - - return nil -} - -func (d *DNSProvider) findZone(ctx context.Context, fqdn string) (*idns.Zone, error) { - resp, _, err := d.client.ZonesAPI.GetZones(ctx).Execute() - if err != nil { - return nil, fmt.Errorf("get zones: %w", err) - } - - if resp == nil { - return nil, errors.New("get zones: no results") - } - - for domain := range dns01.UnFqdnDomainsSeq(fqdn) { - for _, zone := range resp.GetResults() { - if zone.GetDomain() == domain { - return &zone, nil - } - } - } - - return nil, fmt.Errorf("zone not found (fqdn: %q)", fqdn) -} - -// findExistingTXTRecord searches for an existing TXT record with the given name in the specified zone. -// It handles pagination to search through all pages of results. -func (d *DNSProvider) findExistingTXTRecord(ctx context.Context, zoneID int32, recordName string) (*idns.RecordGet, error) { - var page int64 = 1 - - for { - resp, _, err := d.client.RecordsAPI.GetZoneRecords(ctx, zoneID).Page(page).PageSize(int64(d.config.PageSize)).Execute() - if err != nil { - return nil, fmt.Errorf("get zone records (page %d): %w", page, err) - } - - if resp == nil { - return nil, errors.New("get zone records: no results") - } - - results, ok := resp.GetResultsOk() - if !ok || results == nil { - return nil, errors.New("get zone records: empty") - } - - // Search for existing TXT record with the same name in current page - for _, record := range results.GetRecords() { - if record.GetRecordType() == "TXT" && record.GetEntry() == recordName { - return &record, nil - } - } - - // Check if there are more pages to search - if page >= int64(resp.GetTotalPages()) { - break - } - - page++ - } - - // No existing record found in any page - return nil, nil -} - -func authContext(ctx context.Context, key string) context.Context { - return context.WithValue(ctx, idns.ContextAPIKeys, map[string]idns.APIKey{ - "tokenAuth": { - Key: key, - Prefix: "Token", - }, - }) -} - -func extractSubDomain(info dns01.ChallengeInfo, zone *idns.Zone) (string, error) { - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.GetName()) - if err != nil { - return "", err - } - - if subDomain != "" { - return subDomain, nil - } - - return "@", nil -} diff --git a/providers/dns/azion/azion.toml b/providers/dns/azion/azion.toml deleted file mode 100644 index 52df20ab5..000000000 --- a/providers/dns/azion/azion.toml +++ /dev/null @@ -1,24 +0,0 @@ -Name = "Azion" -Description = '''''' -Code = "azion" -Since = "v4.24.0" -URL = "https://www.azion.com/en/products/edge-dns/" - -Example = ''' -AZION_PERSONAL_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --dns azion -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - AZION_PERSONAL_TOKEN = "Your Azion personal token." - [Configuration.Additional] - AZION_PAGE_SIZE = "The page size for the API request (Default: 50)" - AZION_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - AZION_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - AZION_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - AZION_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://api.azion.com/" - GoClient = "https://github.com/aziontech/azionapi-go-sdk" diff --git a/providers/dns/azion/azion_test.go b/providers/dns/azion/azion_test.go deleted file mode 100644 index 517594cdc..000000000 --- a/providers/dns/azion/azion_test.go +++ /dev/null @@ -1,235 +0,0 @@ -package azion - -import ( - "context" - "net/http/httptest" - "testing" - - "github.com/aziontech/azionapi-go-sdk/idns" - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvPersonalToken).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvPersonalToken: "token", - }, - }, - { - desc: "missing credentials", - envVars: map[string]string{ - EnvPersonalToken: "", - }, - expected: "azion: some credentials information are missing: AZION_PERSONAL_TOKEN", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - token string - expected string - }{ - { - desc: "success", - token: "token", - }, - { - desc: "missing credentials", - expected: "azion: missing credentials", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.PersonalToken = test.token - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_findZone(t *testing.T) { - provider := mockBuilder(). - Route("GET /intelligent_dns", servermock.ResponseFromFixture("zones.json")). - Build(t) - - testCases := []struct { - desc string - fqdn string - expected *idns.Zone - }{ - { - desc: "apex", - fqdn: "example.com.", - expected: &idns.Zone{ - Id: idns.PtrInt32(1), - Domain: idns.PtrString("example.com"), - }, - }, - { - desc: "sub domain", - fqdn: "sub.example.com.", - expected: &idns.Zone{ - Id: idns.PtrInt32(2), - Domain: idns.PtrString("sub.example.com"), - }, - }, - { - desc: "long sub domain", - fqdn: "_acme-challenge.api.sub.example.com.", - expected: &idns.Zone{ - Id: idns.PtrInt32(2), - Domain: idns.PtrString("sub.example.com"), - }, - }, - { - desc: "long sub domain, apex", - fqdn: "_acme-challenge.test.example.com.", - expected: &idns.Zone{ - Id: idns.PtrInt32(1), - Domain: idns.PtrString("example.com"), - }, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - zone, err := provider.findZone(context.Background(), test.fqdn) - require.NoError(t, err) - - assert.Equal(t, test.expected, zone) - }) - } -} - -func TestDNSProvider_findZone_error(t *testing.T) { - testCases := []struct { - desc string - fqdn string - response string - expected string - }{ - { - desc: "no parent zone found", - fqdn: "_acme-challenge.example.org.", - response: "zones.json", - expected: `zone not found (fqdn: "_acme-challenge.example.org.")`, - }, - { - desc: "empty zones list", - fqdn: "example.com.", - response: "zones_empty.json", - expected: `zone not found (fqdn: "example.com.")`, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - provider := mockBuilder(). - Route("GET /intelligent_dns", servermock.ResponseFromFixture(test.response)). - Build(t) - - zone, err := provider.findZone(context.Background(), test.fqdn) - require.EqualError(t, err, test.expected) - - assert.Nil(t, zone) - }) - } -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.PersonalToken = "secret" - - provider, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - clientConfig := provider.client.GetConfig() - clientConfig.HTTPClient = server.Client() - clientConfig.Servers = idns.ServerConfigurations{{ - URL: server.URL, - Description: "Production", - }} - - return provider, nil - }, - ) -} diff --git a/providers/dns/azion/fixtures/zones.json b/providers/dns/azion/fixtures/zones.json deleted file mode 100644 index 7dccedf1a..000000000 --- a/providers/dns/azion/fixtures/zones.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "count": 2, - "links": { - "previous": null, - "next": null - }, - "total_pages": 1, - "results": [ - { - "id": 1, - "domain": "example.com" - }, - { - "id": 2, - "domain": "sub.example.com" - } - ], - "schema_version": 3 -} diff --git a/providers/dns/azion/fixtures/zones_empty.json b/providers/dns/azion/fixtures/zones_empty.json deleted file mode 100644 index 540063837..000000000 --- a/providers/dns/azion/fixtures/zones_empty.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "count": 0, - "links": { - "previous": null, - "next": null - }, - "total_pages": 0, - "results": null, - "schema_version": 3 -} diff --git a/providers/dns/azure/azure.go b/providers/dns/azure/azure.go index 8bfc6cfe1..521209abf 100644 --- a/providers/dns/azure/azure.go +++ b/providers/dns/azure/azure.go @@ -8,7 +8,6 @@ import ( "io" "net/http" "net/url" - "strings" "time" "github.com/Azure/go-autorest/autorest" @@ -19,6 +18,8 @@ import ( "github.com/go-acme/lego/v4/providers/dns/internal/errutils" ) +const defaultMetadataEndpoint = "http://169.254.169.254" + // Environment variables names. const ( envNamespace = "AZURE_" @@ -38,16 +39,8 @@ const ( EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) -const EnvLegoAzureBypassDeprecation = "LEGO_AZURE_BYPASS_DEPRECATION" - -const defaultMetadataEndpoint = "http://169.254.169.254" - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { - ZoneName string - // optional if using instance metadata service ClientID string ClientSecret string @@ -70,7 +63,6 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - ZoneName: env.GetOrFile(EnvZoneName), TTL: env.GetOrDefaultInt(EnvTTL, 60), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), @@ -92,7 +84,6 @@ type DNSProvider struct { // If the credentials are _not_ set via the environment, // then it will attempt to get a bearer token via the instance metadata service. // see: https://github.com/Azure/go-autorest/blob/v10.14.0/autorest/azure/auth/auth.go#L38-L42 -// // Deprecated: use azuredns instead. func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() @@ -100,7 +91,6 @@ func NewDNSProvider() (*DNSProvider, error) { environmentName := env.GetOrFile(EnvEnvironment) if environmentName != "" { var environment aazure.Environment - switch environmentName { case "china": environment = aazure.ChinaCloud @@ -129,25 +119,12 @@ func NewDNSProvider() (*DNSProvider, error) { } // NewDNSProviderConfig return a DNSProvider instance configured for Azure. -// // Deprecated: use azuredns instead. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("azure: the configuration of the DNS provider is nil") } - if !env.GetOrDefaultBool(EnvLegoAzureBypassDeprecation, false) { - var msg strings.Builder - - msg.WriteString("azure: ") - msg.WriteString("The `azure` provider has been deprecated since 2023, and replaced by `azuredns` provider. ") - msg.WriteString("It can be TEMPORARILY reactivated by using the environment variable `LEGO_AZURE_BYPASS_DEPRECATION=true`. ") - msg.WriteString("The `azure` provider will be removed in a future release, please migrate to the `azuredns` provider. ") - msg.WriteString("The documentation of the `azuredns` provider can be found at https://go-acme.github.io/lego/dns/azuredns/") - - return nil, errors.New(msg.String()) - } - if config.HTTPClient == nil { config.HTTPClient = &http.Client{Timeout: 5 * time.Second} } @@ -166,7 +143,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if subsID == "" { return nil, errors.New("azure: SubscriptionID is missing") } - config.SubscriptionID = subsID } @@ -179,7 +155,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if resGroup == "" { return nil, errors.New("azure: ResourceGroup is missing") } - config.ResourceGroup = resGroup } diff --git a/providers/dns/azure/azure.toml b/providers/dns/azure/azure.toml index a38ed55ab..c4e3b674a 100644 --- a/providers/dns/azure/azure.toml +++ b/providers/dns/azure/azure.toml @@ -19,9 +19,9 @@ Example = '''''' AZURE_METADATA_ENDPOINT = "Metadata Service endpoint URL" AZURE_PRIVATE_ZONE = "Set to true to use Azure Private DNS Zones and not public" AZURE_ZONE_NAME = "Zone name to use inside Azure DNS service to add the TXT record in" - AZURE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - AZURE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - AZURE_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" + AZURE_POLLING_INTERVAL = "Time between DNS propagation check" + AZURE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + AZURE_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://docs.microsoft.com/en-us/go/azure/" diff --git a/providers/dns/azure/azure_test.go b/providers/dns/azure/azure_test.go index c4fec4359..496168362 100644 --- a/providers/dns/azure/azure_test.go +++ b/providers/dns/azure/azure_test.go @@ -14,7 +14,6 @@ import ( const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( - EnvLegoAzureBypassDeprecation, EnvEnvironment, EnvClientID, EnvClientSecret, @@ -55,11 +54,8 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() - test.envVars[EnvLegoAzureBypassDeprecation] = "true" - envTest.Apply(test.envVars) p, err := NewDNSProvider() @@ -143,11 +139,6 @@ func TestNewDNSProviderConfig(t *testing.T) { }, } - defer envTest.RestoreEnv() - - envTest.ClearEnv() - envTest.Apply(map[string]string{EnvLegoAzureBypassDeprecation: "true"}) - for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() @@ -167,7 +158,6 @@ func TestNewDNSProviderConfig(t *testing.T) { } else { mux.HandleFunc("/", test.handler) } - config.MetadataEndpoint = server.URL p, err := NewDNSProviderConfig(config) @@ -196,7 +186,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -210,7 +199,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/azure/private.go b/providers/dns/azure/private.go index f7c6a75b7..e07af4e37 100644 --- a/providers/dns/azure/private.go +++ b/providers/dns/azure/private.go @@ -11,6 +11,7 @@ import ( "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/to" "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" ) // dnsProviderPrivate implements the challenge.Provider interface for Azure Private Zone DNS. @@ -54,7 +55,6 @@ func (d *dnsProviderPrivate) Present(domain, token, keyAuth string) error { // Construct unique TXT records using map uniqRecords := map[string]struct{}{info.Value: {}} - if rset.RecordSetProperties != nil && rset.TxtRecords != nil { for _, txtRecord := range *rset.TxtRecords { // Assume Value doesn't contain multiple strings @@ -82,7 +82,6 @@ func (d *dnsProviderPrivate) Present(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("azure: %w", err) } - return nil } @@ -108,14 +107,13 @@ func (d *dnsProviderPrivate) CleanUp(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("azure: %w", err) } - return nil } // Checks that azure has a zone for this domain name. func (d *dnsProviderPrivate) getHostedZoneID(ctx context.Context, fqdn string) (string, error) { - if d.config.ZoneName != "" { - return d.config.ZoneName, nil + if zone := env.GetOrFile(EnvZoneName); zone != "" { + return zone, nil } authZone, err := dns01.FindZoneByFqdn(fqdn) diff --git a/providers/dns/azure/public.go b/providers/dns/azure/public.go index 194956c9c..66b458be9 100644 --- a/providers/dns/azure/public.go +++ b/providers/dns/azure/public.go @@ -11,6 +11,7 @@ import ( "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/to" "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" ) // dnsProviderPublic implements the challenge.Provider interface for Azure Public Zone DNS. @@ -54,7 +55,6 @@ func (d *dnsProviderPublic) Present(domain, token, keyAuth string) error { // Construct unique TXT records using map uniqRecords := map[string]struct{}{info.Value: {}} - if rset.RecordSetProperties != nil && rset.TxtRecords != nil { for _, txtRecord := range *rset.TxtRecords { // Assume Value doesn't contain multiple strings @@ -82,7 +82,6 @@ func (d *dnsProviderPublic) Present(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("azure: %w", err) } - return nil } @@ -108,14 +107,13 @@ func (d *dnsProviderPublic) CleanUp(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("azure: %w", err) } - return nil } // Checks that azure has a zone for this domain name. func (d *dnsProviderPublic) getHostedZoneID(ctx context.Context, fqdn string) (string, error) { - if d.config.ZoneName != "" { - return d.config.ZoneName, nil + if zone := env.GetOrFile(EnvZoneName); zone != "" { + return zone, nil } authZone, err := dns01.FindZoneByFqdn(fqdn) diff --git a/providers/dns/azuredns/azuredns.go b/providers/dns/azuredns/azuredns.go index b8effadea..bd87d9506 100644 --- a/providers/dns/azuredns/azuredns.go +++ b/providers/dns/azuredns/azuredns.go @@ -3,15 +3,20 @@ package azuredns import ( + "context" "errors" "fmt" "net/http" + "strings" "time" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -28,21 +33,10 @@ const ( EnvClientID = envNamespace + "CLIENT_ID" EnvClientSecret = envNamespace + "CLIENT_SECRET" - EnvOIDCToken = envNamespace + "OIDC_TOKEN" - EnvOIDCTokenFilePath = envNamespace + "OIDC_TOKEN_FILE_PATH" - EnvOIDCRequestURL = envNamespace + "OIDC_REQUEST_URL" - EnvGitHubOIDCRequestURL = "ACTIONS_ID_TOKEN_REQUEST_URL" - altEnvArmOIDCRequestURL = "ARM_OIDC_REQUEST_URL" - EnvOIDCRequestToken = envNamespace + "OIDC_REQUEST_TOKEN" - EnvGitHubOIDCRequestToken = "ACTIONS_ID_TOKEN_REQUEST_TOKEN" - altEnvArmOIDCRequestToken = "ARM_OIDC_REQUEST_TOKEN" - - EnvServiceConnectionID = envNamespace + "SERVICE_CONNECTION_ID" - altEnvServiceConnectionID = "SERVICE_CONNECTION_ID" - altEnvArmAdoPipelineServiceConnectionID = "ARM_ADO_PIPELINE_SERVICE_CONNECTION_ID" - altEnvArmOIDCAzureServiceConnectionID = "ARM_OIDC_AZURE_SERVICE_CONNECTION_ID" - EnvSystemAccessToken = envNamespace + "SYSTEM_ACCESS_TOKEN" - altEnvSystemAccessToken = "SYSTEM_ACCESSTOKEN" + EnvOIDCToken = envNamespace + "OIDC_TOKEN" + EnvOIDCTokenFilePath = envNamespace + "OIDC_TOKEN_FILE_PATH" + EnvOIDCRequestURL = envNamespace + "OIDC_REQUEST_URL" + EnvOIDCRequestToken = envNamespace + "OIDC_REQUEST_TOKEN" EnvAuthMethod = envNamespace + "AUTH_METHOD" EnvAuthMSITimeout = envNamespace + "AUTH_MSI_TIMEOUT" @@ -52,14 +46,13 @@ const ( EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" -) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + EnvGitHubOIDCRequestURL = "ACTIONS_ID_TOKEN_REQUEST_URL" + EnvGitHubOIDCRequestToken = "ACTIONS_ID_TOKEN_REQUEST_TOKEN" +) // Config is used to configure the creation of the DNSProvider. type Config struct { - ZoneName string - SubscriptionID string ResourceGroup string PrivateZone bool @@ -76,9 +69,6 @@ type Config struct { OIDCRequestURL string OIDCRequestToken string - ServiceConnectionID string - SystemAccessToken string - AuthMethod string AuthMSITimeout time.Duration @@ -93,7 +83,6 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - ZoneName: env.GetOrFile(EnvZoneName), TTL: env.GetOrDefaultInt(EnvTTL, 60), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), @@ -140,22 +129,13 @@ func NewDNSProvider() (*DNSProvider, error) { config.ServiceDiscoveryFilter = env.GetOrFile(EnvServiceDiscoveryFilter) oidcValues, _ := env.GetWithFallback( - []string{EnvOIDCRequestURL, EnvGitHubOIDCRequestURL, altEnvArmOIDCRequestURL}, - []string{EnvOIDCRequestToken, EnvGitHubOIDCRequestToken, altEnvArmOIDCRequestToken}, + []string{EnvOIDCRequestURL, EnvGitHubOIDCRequestURL}, + []string{EnvOIDCRequestToken, EnvGitHubOIDCRequestToken}, ) config.OIDCRequestURL = oidcValues[EnvOIDCRequestURL] config.OIDCRequestToken = oidcValues[EnvOIDCRequestToken] - // https://registry.terraform.io/providers/hashicorp/Azurerm/latest/docs/guides/service_principal_oidc - pipelineValues, _ := env.GetWithFallback( - []string{EnvServiceConnectionID, altEnvServiceConnectionID, altEnvArmAdoPipelineServiceConnectionID, altEnvArmOIDCAzureServiceConnectionID}, - []string{EnvSystemAccessToken, altEnvArmOIDCRequestToken, altEnvSystemAccessToken}, - ) - - config.ServiceConnectionID = pipelineValues[EnvServiceConnectionID] - config.SystemAccessToken = pipelineValues[EnvSystemAccessToken] - config.AuthMethod = env.GetOrFile(EnvAuthMethod) config.AuthMSITimeout = env.GetOrDefaultSecond(EnvAuthMSITimeout, 2*time.Second) @@ -172,8 +152,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { config.HTTPClient = &http.Client{Timeout: 5 * time.Second} } - config.HTTPClient = clientdebug.Wrap(config.HTTPClient) - credentials, err := getCredentials(config) if err != nil { return nil, fmt.Errorf("azuredns: Unable to retrieve valid credentials: %w", err) @@ -210,3 +188,94 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return d.provider.CleanUp(domain, token, keyAuth) } + +func getCredentials(config *Config) (azcore.TokenCredential, error) { + clientOptions := azcore.ClientOptions{Cloud: config.Environment} + + switch strings.ToLower(config.AuthMethod) { + case "env": + if config.ClientID != "" && config.ClientSecret != "" && config.TenantID != "" { + return azidentity.NewClientSecretCredential(config.TenantID, config.ClientID, config.ClientSecret, + &azidentity.ClientSecretCredentialOptions{ClientOptions: clientOptions}) + } + + return azidentity.NewEnvironmentCredential(&azidentity.EnvironmentCredentialOptions{ClientOptions: clientOptions}) + + case "wli": + return azidentity.NewWorkloadIdentityCredential(&azidentity.WorkloadIdentityCredentialOptions{ClientOptions: clientOptions}) + + case "msi": + cred, err := azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{ClientOptions: clientOptions}) + if err != nil { + return nil, err + } + + return &timeoutTokenCredential{cred: cred, timeout: config.AuthMSITimeout}, nil + + case "cli": + var credOptions *azidentity.AzureCLICredentialOptions + if config.TenantID != "" { + credOptions = &azidentity.AzureCLICredentialOptions{TenantID: config.TenantID} + } + return azidentity.NewAzureCLICredential(credOptions) + + case "oidc": + err := checkOIDCConfig(config) + if err != nil { + return nil, err + } + + return azidentity.NewClientAssertionCredential(config.TenantID, config.ClientID, getOIDCAssertion(config), &azidentity.ClientAssertionCredentialOptions{ClientOptions: clientOptions}) + + default: + return azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{ClientOptions: clientOptions}) + } +} + +// timeoutTokenCredential wraps a TokenCredential to add a timeout. +type timeoutTokenCredential struct { + cred azcore.TokenCredential + timeout time.Duration +} + +// GetToken implements the azcore.TokenCredential interface. +func (w *timeoutTokenCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) { + if w.timeout <= 0 { + return w.cred.GetToken(ctx, opts) + } + + ctxTimeout, cancel := context.WithTimeout(ctx, w.timeout) + defer cancel() + + tk, err := w.cred.GetToken(ctxTimeout, opts) + if ce := ctxTimeout.Err(); errors.Is(ce, context.DeadlineExceeded) { + return tk, azidentity.NewCredentialUnavailableError("managed identity timed out") + } + + w.timeout = 0 + + return tk, err +} + +func getAuthZone(fqdn string) (string, error) { + authZone := env.GetOrFile(EnvZoneName) + if authZone != "" { + return authZone, nil + } + + authZone, err := dns01.FindZoneByFqdn(fqdn) + if err != nil { + return "", fmt.Errorf("could not find zone: %w", err) + } + + return authZone, nil +} + +func deref[T any](v *T) T { + if v == nil { + var zero T + return zero + } + + return *v +} diff --git a/providers/dns/azuredns/azuredns.toml b/providers/dns/azuredns/azuredns.toml index 7c800ce7e..7cd1b5814 100644 --- a/providers/dns/azuredns/azuredns.toml +++ b/providers/dns/azuredns/azuredns.toml @@ -10,32 +10,32 @@ Example = ''' AZURE_CLIENT_ID= \ AZURE_TENANT_ID= \ AZURE_CLIENT_SECRET= \ -lego --dns azuredns -d '*.example.com' -d example.com run +lego --domains example.com --email your_example@email.com --dns azuredns run ### Using client certificate AZURE_CLIENT_ID= \ AZURE_TENANT_ID= \ AZURE_CLIENT_CERTIFICATE_PATH= \ -lego --dns azuredns -d '*.example.com' -d example.com run +lego --domains example.com --email your_example@email.com --dns azuredns run ### Using Azure CLI az login \ -lego --dns azuredns -d '*.example.com' -d example.com run +lego --domains example.com --email your_example@email.com --dns azuredns run ### Using Managed Identity (Azure VM) AZURE_TENANT_ID= \ AZURE_RESOURCE_GROUP= \ -lego --dns azuredns -d '*.example.com' -d example.com run +lego --domains example.com --email your_example@email.com --dns azuredns run ### Using Managed Identity (Azure Arc) AZURE_TENANT_ID= \ IMDS_ENDPOINT=http://localhost:40342 \ IDENTITY_ENDPOINT=http://localhost:40342/metadata/identity/oauth2/token \ -lego --dns azuredns -d '*.example.com' -d example.com run +lego --domains example.com --email your_example@email.com --dns azuredns run ''' @@ -174,10 +174,6 @@ This authentication method can be specifically used by setting the `AZURE_AUTH_M Open ID Connect is a mechanism that establish a trust relationship between a running environment and the Azure AD identity provider. It can be enabled by setting the `AZURE_AUTH_METHOD` environment variable to `oidc`. -### Azure DevOps Pipelines - -It can be enabled by setting the `AZURE_AUTH_METHOD` environment variable to `pipeline`. - ''' [Configuration] @@ -195,9 +191,9 @@ It can be enabled by setting the `AZURE_AUTH_METHOD` environment variable to `pi AZURE_ZONE_NAME = "Zone name to use inside Azure DNS service to add the TXT record in" AZURE_AUTH_METHOD = "Specify which authentication method to use" AZURE_AUTH_MSI_TIMEOUT = "Managed Identity timeout duration" - AZURE_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" - AZURE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - AZURE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" + AZURE_TTL = "The TTL of the TXT record used for the DNS challenge" + AZURE_POLLING_INTERVAL = "Time between DNS propagation check" + AZURE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" [Links] API = "https://docs.microsoft.com/en-us/go/azure/" diff --git a/providers/dns/azuredns/azuredns_test.go b/providers/dns/azuredns/azuredns_test.go index 594a0d6a3..7ddb4de45 100644 --- a/providers/dns/azuredns/azuredns_test.go +++ b/providers/dns/azuredns/azuredns_test.go @@ -35,7 +35,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -62,7 +61,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -76,7 +74,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/azuredns/credentials.go b/providers/dns/azuredns/credentials.go deleted file mode 100644 index a38b3f7dd..000000000 --- a/providers/dns/azuredns/credentials.go +++ /dev/null @@ -1,136 +0,0 @@ -package azuredns - -import ( - "context" - "errors" - "fmt" - "strings" - "time" - - "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" - "github.com/Azure/azure-sdk-for-go/sdk/azidentity" - "github.com/go-acme/lego/v4/challenge/dns01" -) - -const ( - authMethodEnv = "env" - authMethodWLI = "wli" - authMethodMSI = "msi" - authMethodCLI = "cli" - authMethodOIDC = "oidc" - authMethodPipeline = "pipeline" -) - -//nolint:gocyclo // The complexity is related to the number of possible configurations. -func getCredentials(config *Config) (azcore.TokenCredential, error) { - clientOptions := azcore.ClientOptions{Cloud: config.Environment} - - switch strings.ToLower(config.AuthMethod) { - case authMethodEnv: - if config.ClientID != "" && config.ClientSecret != "" && config.TenantID != "" { - return azidentity.NewClientSecretCredential(config.TenantID, config.ClientID, config.ClientSecret, - &azidentity.ClientSecretCredentialOptions{ClientOptions: clientOptions}) - } - - return azidentity.NewEnvironmentCredential(&azidentity.EnvironmentCredentialOptions{ClientOptions: clientOptions}) - - case authMethodWLI: - return azidentity.NewWorkloadIdentityCredential(&azidentity.WorkloadIdentityCredentialOptions{ClientOptions: clientOptions}) - - case authMethodMSI: - cred, err := azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{ClientOptions: clientOptions}) - if err != nil { - return nil, err - } - - return &timeoutTokenCredential{cred: cred, timeout: config.AuthMSITimeout}, nil - - case authMethodCLI: - var credOptions *azidentity.AzureCLICredentialOptions - if config.TenantID != "" { - credOptions = &azidentity.AzureCLICredentialOptions{TenantID: config.TenantID} - } - - return azidentity.NewAzureCLICredential(credOptions) - - case authMethodOIDC: - err := checkOIDCConfig(config) - if err != nil { - return nil, err - } - - return azidentity.NewClientAssertionCredential(config.TenantID, config.ClientID, getOIDCAssertion(config), &azidentity.ClientAssertionCredentialOptions{ClientOptions: clientOptions}) - - case authMethodPipeline: - err := checkPipelineConfig(config) - if err != nil { - return nil, err - } - - // Uses the env var `SYSTEM_OIDCREQUESTURI`, - // but the constant is not exported, - // and there is no way to set it programmatically. - // https://github.com/Azure/azure-sdk-for-go/blob/aae2fb75ffccafc669db72bebc3c1a66332f48d7/sdk/azidentity/azure_pipelines_credential.go#L22 - // https://github.com/Azure/azure-sdk-for-go/blob/aae2fb75ffccafc669db72bebc3c1a66332f48d7/sdk/azidentity/azure_pipelines_credential.go#L79 - - return azidentity.NewAzurePipelinesCredential(config.TenantID, config.ClientID, config.ServiceConnectionID, config.SystemAccessToken, &azidentity.AzurePipelinesCredentialOptions{ClientOptions: clientOptions}) - - default: - return azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{ClientOptions: clientOptions}) - } -} - -// timeoutTokenCredential wraps a TokenCredential to add a timeout. -type timeoutTokenCredential struct { - cred azcore.TokenCredential - timeout time.Duration -} - -// GetToken implements the azcore.TokenCredential interface. -func (w *timeoutTokenCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) { - if w.timeout <= 0 { - return w.cred.GetToken(ctx, opts) - } - - ctxTimeout, cancel := context.WithTimeout(ctx, w.timeout) - defer cancel() - - tk, err := w.cred.GetToken(ctxTimeout, opts) - if ce := ctxTimeout.Err(); errors.Is(ce, context.DeadlineExceeded) { - return tk, azidentity.NewCredentialUnavailableError("managed identity timed out") - } - - w.timeout = 0 - - return tk, err -} - -func getZoneName(config *Config, fqdn string) (string, error) { - if config.ZoneName != "" { - return config.ZoneName, nil - } - - authZone, err := dns01.FindZoneByFqdn(fqdn) - if err != nil { - return "", fmt.Errorf("could not find zone for %s: %w", fqdn, err) - } - - if authZone == "" { - return "", errors.New("empty zone name") - } - - return authZone, nil -} - -func checkPipelineConfig(config *Config) error { - if config.ServiceConnectionID == "" { - return errors.New("azuredns: ServiceConnectionID is missing") - } - - if config.SystemAccessToken == "" { - return errors.New("azuredns: SystemAccessToken is missing") - } - - return nil -} diff --git a/providers/dns/azuredns/private.go b/providers/dns/azuredns/private.go index 43b39ed14..516879f30 100644 --- a/providers/dns/azuredns/private.go +++ b/providers/dns/azuredns/private.go @@ -12,13 +12,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/providers/dns/internal/ptr" ) -var _ challenge.ProviderTimeout = (*DNSProviderPrivate)(nil) - // DNSProviderPrivate implements the challenge.Provider interface for Azure Private Zone DNS. type DNSProviderPrivate struct { config *Config @@ -129,7 +125,7 @@ func (d *DNSProviderPrivate) CleanUp(domain, _, keyAuth string) error { // Checks that azure has a zone for this domain name. func (d *DNSProviderPrivate) getHostedZone(fqdn string) (ServiceDiscoveryZone, error) { - authZone, err := getZoneName(d.config, fqdn) + authZone, err := getAuthZone(fqdn) if err != nil { return ServiceDiscoveryZone{}, err } @@ -181,12 +177,11 @@ func (c privateZoneClient) Delete(ctx context.Context, subDomain string) (armpri func privateUniqueRecords(recordSet armprivatedns.RecordSet, value string) map[string]struct{} { uniqRecords := map[string]struct{}{value: {}} - if recordSet.Properties != nil && recordSet.Properties.TxtRecords != nil { for _, txtRecord := range recordSet.Properties.TxtRecords { // Assume Value doesn't contain multiple strings if len(txtRecord.Value) > 0 { - uniqRecords[ptr.Deref(txtRecord.Value[0])] = struct{}{} + uniqRecords[deref(txtRecord.Value[0])] = struct{}{} } } } diff --git a/providers/dns/azuredns/public.go b/providers/dns/azuredns/public.go index 79b6e783f..0d0af53a8 100644 --- a/providers/dns/azuredns/public.go +++ b/providers/dns/azuredns/public.go @@ -12,13 +12,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/providers/dns/internal/ptr" ) -var _ challenge.ProviderTimeout = (*DNSProviderPublic)(nil) - // DNSProviderPublic implements the challenge.Provider interface for Azure Public Zone DNS. type DNSProviderPublic struct { config *Config @@ -128,7 +124,7 @@ func (d *DNSProviderPublic) CleanUp(domain, _, keyAuth string) error { // Checks that azure has a zone for this domain name. func (d *DNSProviderPublic) getHostedZone(fqdn string) (ServiceDiscoveryZone, error) { - authZone, err := getZoneName(d.config, fqdn) + authZone, err := getAuthZone(fqdn) if err != nil { return ServiceDiscoveryZone{}, err } @@ -179,12 +175,11 @@ func (c publicZoneClient) Delete(ctx context.Context, subDomain string) (armdns. func publicUniqueRecords(recordSet armdns.RecordSet, value string) map[string]struct{} { uniqRecords := map[string]struct{}{value: {}} - if recordSet.Properties != nil && recordSet.Properties.TxtRecords != nil { for _, txtRecord := range recordSet.Properties.TxtRecords { // Assume Value doesn't contain multiple strings if len(txtRecord.Value) > 0 { - uniqRecords[ptr.Deref(txtRecord.Value[0])] = struct{}{} + uniqRecords[deref(txtRecord.Value[0])] = struct{}{} } } } diff --git a/providers/dns/azuredns/servicediscovery.go b/providers/dns/azuredns/servicediscovery.go index 50a41da37..62dfd6623 100644 --- a/providers/dns/azuredns/servicediscovery.go +++ b/providers/dns/azuredns/servicediscovery.go @@ -9,7 +9,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph" - "github.com/go-acme/lego/v4/providers/dns/internal/ptr" ) type ServiceDiscoveryZone struct { @@ -46,7 +45,6 @@ func discoverDNSZones(ctx context.Context, config *Config, credentials azcore.To } zones := map[string]ServiceDiscoveryZone{} - for { // create the query request request := armresourcegraph.QueryRequest{ @@ -90,7 +88,7 @@ func discoverDNSZones(ctx context.Context, config *Config, credentials azcore.To *requestOptions.Skip += ResourceGraphQueryOptionsTop if result.TotalRecords != nil { - if int64(ptr.Deref(requestOptions.Skip)) >= ptr.Deref(result.TotalRecords) { + if int64(deref(requestOptions.Skip)) >= deref(result.TotalRecords) { break } } diff --git a/providers/dns/baiducloud/baiducloud.go b/providers/dns/baiducloud/baiducloud.go deleted file mode 100644 index 1dc8d90ed..000000000 --- a/providers/dns/baiducloud/baiducloud.go +++ /dev/null @@ -1,171 +0,0 @@ -// Package baiducloud implements a DNS provider for solving the DNS-01 challenge using Baidu Cloud. -package baiducloud - -import ( - "errors" - "fmt" - "time" - - baidudns "github.com/baidubce/bce-sdk-go/services/dns" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/ptr" -) - -// Environment variables names. -const ( - envNamespace = "BAIDUCLOUD_" - - EnvAccessKeyID = envNamespace + "ACCESS_KEY_ID" - EnvSecretAccessKey = envNamespace + "SECRET_ACCESS_KEY" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" -) - -// 300 is the minimum TTL for free users. -const defaultTTL = 300 - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - AccessKeyID string - SecretAccessKey string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, defaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *baidudns.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for Baidu Cloud. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAccessKeyID, EnvSecretAccessKey) - if err != nil { - return nil, fmt.Errorf("baiducloud: %w", err) - } - - config := NewDefaultConfig() - config.AccessKeyID = values[EnvAccessKeyID] - config.SecretAccessKey = values[EnvSecretAccessKey] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Baidu Cloud. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("baiducloud: the configuration of the DNS provider is nil") - } - - if config.AccessKeyID == "" && config.SecretAccessKey == "" { - return nil, errors.New("baiducloud: credentials missing") - } - - client, err := baidudns.NewClient(config.AccessKeyID, config.SecretAccessKey, "") - if err != nil { - return nil, fmt.Errorf("baiducloud: %w", err) - } - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("baiducloud: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("baiducloud: %w", err) - } - - crr := &baidudns.CreateRecordRequest{ - Description: ptr.Pointer("lego"), - Rr: subDomain, - Type: "TXT", - Value: info.Value, - Ttl: ptr.Pointer(int32(d.config.TTL)), - } - - err = d.client.CreateRecord(dns01.UnFqdn(authZone), crr, "") - if err != nil { - return fmt.Errorf("baiducloud: create record: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("baiducloud: could not find zone for domain %q: %w", domain, err) - } - - recordID, err := d.findRecordID(dns01.UnFqdn(authZone), info.Value) - if err != nil { - return fmt.Errorf("baiducloud: find record: %w", err) - } - - err = d.client.DeleteRecord(dns01.UnFqdn(authZone), recordID, "") - if err != nil { - return fmt.Errorf("baiducloud: delete record: %w", err) - } - - return nil -} - -func (d *DNSProvider) findRecordID(zoneName, tokenValue string) (string, error) { - lrr := &baidudns.ListRecordRequest{} - - for { - recordResponse, err := d.client.ListRecord(zoneName, lrr) - if err != nil { - return "", fmt.Errorf("baiducloud: list record: %w", err) - } - - for _, record := range recordResponse.Records { - if record.Type == "TXT" && record.Value == tokenValue { - return record.Id, nil - } - } - - if !recordResponse.IsTruncated { - break - } - - lrr.Marker = recordResponse.NextMarker - } - - return "", errors.New("record not found") -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/baiducloud/baiducloud.toml b/providers/dns/baiducloud/baiducloud.toml deleted file mode 100644 index 54f1f6312..000000000 --- a/providers/dns/baiducloud/baiducloud.toml +++ /dev/null @@ -1,24 +0,0 @@ -Name = "Baidu Cloud" -Description = '''''' -URL = "https://cloud.baidu.com" -Code = "baiducloud" -Since = "v4.23.0" - -Example = ''' -BAIDUCLOUD_ACCESS_KEY_ID="xxx" \ -BAIDUCLOUD_SECRET_ACCESS_KEY="yyy" \ -lego --dns baiducloud -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - BAIDUCLOUD_ACCESS_KEY_ID = "Access key" - BAIDUCLOUD_SECRET_ACCESS_KEY = "Secret access key" - [Configuration.Additional] - BAIDUCLOUD_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - BAIDUCLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - BAIDUCLOUD_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - -[Links] - API = "https://cloud.baidu.com/doc/DNS/s/El4s7lssr" - GoClient = "https://github.com/baidubce/bce-sdk-go" diff --git a/providers/dns/baiducloud/baiducloud_test.go b/providers/dns/baiducloud/baiducloud_test.go deleted file mode 100644 index 483bfaf5e..000000000 --- a/providers/dns/baiducloud/baiducloud_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package baiducloud - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAccessKeyID, EnvSecretAccessKey).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAccessKeyID: "key", - EnvSecretAccessKey: "secret", - }, - }, - { - desc: "missing access key ID", - envVars: map[string]string{ - EnvAccessKeyID: "key", - }, - expected: "baiducloud: some credentials information are missing: BAIDUCLOUD_SECRET_ACCESS_KEY", - }, - { - desc: "missing secret access key", - envVars: map[string]string{ - EnvSecretAccessKey: "secret", - }, - expected: "baiducloud: some credentials information are missing: BAIDUCLOUD_ACCESS_KEY_ID", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "baiducloud: some credentials information are missing: BAIDUCLOUD_ACCESS_KEY_ID,BAIDUCLOUD_SECRET_ACCESS_KEY", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - accessKeyID string - secretAccessKey string - expected string - }{ - { - desc: "success", - accessKeyID: "key", - secretAccessKey: "secret", - }, - { - desc: "missing access key ID", - accessKeyID: "", - secretAccessKey: "secret", - expected: "baiducloud: accessKeyId should not be empty", - }, - { - desc: "missing secret access key", - accessKeyID: "key", - secretAccessKey: "", - expected: "baiducloud: secretKey should not be empty", - }, - { - desc: "missing credentials", - expected: "baiducloud: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.AccessKeyID = test.accessKeyID - config.SecretAccessKey = test.secretAccessKey - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/beget/beget.go b/providers/dns/beget/beget.go deleted file mode 100644 index d4449deb8..000000000 --- a/providers/dns/beget/beget.go +++ /dev/null @@ -1,164 +0,0 @@ -// Package beget implements a DNS provider for solving the DNS-01 challenge using beget.com DNS. -package beget - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/beget/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "BEGET_" - - EnvUsername = envNamespace + "USERNAME" - EnvPassword = envNamespace + "PASSWORD" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - Username string - Password string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, 300), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 30*time.Second), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for beget.com. -// Credentials must be passed in the environment variables: -// BEGET_USERNAME and BEGET_PASSWORD. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvUsername, EnvPassword) - if err != nil { - return nil, fmt.Errorf("beget: %w", err) - } - - config := NewDefaultConfig() - config.Username = values[EnvUsername] - config.Password = values[EnvPassword] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for beget.com. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("beget: the configuration of the DNS provider is nil") - } - - if config.Username == "" || config.Password == "" { - return nil, errors.New("beget: incomplete credentials, missing username and/or password") - } - - client := internal.NewClient(config.Username, config.Password) - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{config: config, client: client}, nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - records, err := d.client.GetTXTRecords(ctx, dns01.UnFqdn(info.EffectiveFQDN)) - if err != nil { - return fmt.Errorf("beget: get TXT records: %w", err) - } - - records = append(records, internal.Record{ - Value: info.Value, - Data: "", // NOTE: there are 2 fields in the API for the same thing. - Priority: 10, - TTL: d.config.TTL, - }) - - err = d.client.ChangeTXTRecord(ctx, dns01.UnFqdn(info.EffectiveFQDN), records) - if err != nil { - return fmt.Errorf("beget: failed to create TXT records [domain: %s]: %w", - dns01.UnFqdn(info.EffectiveFQDN), err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - records, err := d.client.GetTXTRecords(ctx, dns01.UnFqdn(info.EffectiveFQDN)) - if err != nil { - return fmt.Errorf("beget: get TXT records: %w", err) - } - - if len(records) == 0 { - return nil - } - - var updatedRecords []internal.Record - - for _, record := range records { - if record.Data == info.Value { - continue - } - - updatedRecords = append(updatedRecords, record) - } - - err = d.client.ChangeTXTRecord(ctx, dns01.UnFqdn(info.EffectiveFQDN), updatedRecords) - if err != nil { - return fmt.Errorf("beget: failed to remove TXT records [domain: %s]: %w", - dns01.UnFqdn(info.EffectiveFQDN), err) - } - - return nil -} diff --git a/providers/dns/beget/beget.toml b/providers/dns/beget/beget.toml deleted file mode 100644 index 4ed26d850..000000000 --- a/providers/dns/beget/beget.toml +++ /dev/null @@ -1,24 +0,0 @@ -Name = "Beget.com" -Description = '''''' -URL = "https://beget.com/" -Code = "beget" -Since = "v4.27.0" - -Example = ''' -BEGET_USERNAME=xxxxxx \ -BEGET_PASSWORD=yyyyyy \ -lego --dns beget -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - BEGET_USERNAME = "API username" - BEGET_PASSWORD = "API password" - [Configuration.Additional] - BEGET_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 30)" - BEGET_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 300)" - BEGET_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - BEGET_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://beget.com/ru/kb/api/funkczii-upravleniya-dns" diff --git a/providers/dns/beget/beget_test.go b/providers/dns/beget/beget_test.go deleted file mode 100644 index 3cfb3c0b4..000000000 --- a/providers/dns/beget/beget_test.go +++ /dev/null @@ -1,232 +0,0 @@ -package beget - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvUsername, EnvPassword).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvUsername: "123", - EnvPassword: "456", - }, - }, - { - desc: "missing credentials", - envVars: map[string]string{ - EnvUsername: "", - EnvPassword: "", - }, - expected: "beget: some credentials information are missing: BEGET_USERNAME,BEGET_PASSWORD", - }, - { - desc: "missing username", - envVars: map[string]string{ - EnvUsername: "", - EnvPassword: "456", - }, - expected: "beget: some credentials information are missing: BEGET_USERNAME", - }, - { - desc: "missing password", - envVars: map[string]string{ - EnvUsername: "123", - EnvPassword: "", - }, - expected: "beget: some credentials information are missing: BEGET_PASSWORD", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - username string - password string - expected string - }{ - { - desc: "success", - username: "123", - password: "456", - }, - { - desc: "missing credentials", - username: "", - password: "", - expected: "beget: incomplete credentials, missing username and/or password", - }, - { - desc: "missing username", - username: "", - password: "456", - expected: "beget: incomplete credentials, missing username and/or password", - }, - { - desc: "missing password", - username: "123", - password: "", - expected: "beget: incomplete credentials, missing username and/or password", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.Username = test.username - config.Password = test.password - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - assert.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - assert.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - assert.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - assert.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.Username = "user" - config.Password = "secret" - config.HTTPClient = server.Client() - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - p.client.BaseURL, _ = url.Parse(server.URL) - - return p, nil - }, - servermock.CheckQueryParameter(). - With("login", "user"). - With("passwd", "secret"). - With("input_format", "json"). - With("output_format", "json"), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("GET /dns/getData", - servermock.ResponseFromInternal("getData-real.json"), - servermock.CheckQueryParameter(). - With("input_data", `{"fqdn":"_acme-challenge.example.com"}`), - ). - Route("GET /dns/changeRecords", - servermock.ResponseFromInternal("changeRecords-doc.json"), - servermock.CheckQueryParameter(). - With("input_data", `{"fqdn":"_acme-challenge.example.com","records":{"TXT":[{"txtdata":"v=spf1 redirect=beget.com","ttl":300},{"value":"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY","priority":10,"ttl":300}]}}`), - ). - Build(t) - - err := provider.Present("example.com", "", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("GET /dns/getData", - servermock.ResponseFromInternal("getData.json"), - servermock.CheckQueryParameter(). - With("input_data", `{"fqdn":"_acme-challenge.example.com"}`), - ). - Route("GET /dns/changeRecords", - servermock.ResponseFromInternal("changeRecords-doc.json"), - servermock.CheckQueryParameter(). - With("input_data", `{"fqdn":"_acme-challenge.example.com","records":{"TXT":[{"txtdata":"foo","ttl":300}]}}`), - ). - Build(t) - - err := provider.CleanUp("example.com", "", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp_empty(t *testing.T) { - provider := mockBuilder(). - Route("GET /dns/getData", - servermock.ResponseFromInternal("getData_empty.json"), - servermock.CheckQueryParameter(). - With("input_data", `{"fqdn":"_acme-challenge.example.com"}`), - ). - Route("/", - servermock.Noop().WithStatusCode(http.StatusInternalServerError)). - Build(t) - - err := provider.CleanUp("example.com", "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/beget/internal/client.go b/providers/dns/beget/internal/client.go deleted file mode 100644 index 9b9746ba2..000000000 --- a/providers/dns/beget/internal/client.go +++ /dev/null @@ -1,137 +0,0 @@ -package internal - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" -) - -const defaultBaseURL = "https://api.beget.com/api/" - -// Client the beget.com client. -type Client struct { - login string - password string - - BaseURL *url.URL - HTTPClient *http.Client -} - -// NewClient Creates a beget.com client. -func NewClient(login, password string) *Client { - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - login: login, - password: password, - BaseURL: baseURL, - HTTPClient: &http.Client{Timeout: 5 * time.Second}, - } -} - -// GetTXTRecords returns TXT records. -// https://beget.com/ru/kb/api/funkczii-upravleniya-dns#getdata -func (c *Client) GetTXTRecords(ctx context.Context, domain string) ([]Record, error) { - request := GetRecordsRequest{Fqdn: domain} - - resp, err := c.doRequest(ctx, request, "dns", "getData") - if err != nil { - return nil, err - } - - err = resp.HasError() - if err != nil { - return nil, err - } - - result := GetRecordsResult{} - - err = json.Unmarshal(resp.Answer.Result, &result) - if err != nil { - return nil, fmt.Errorf("unmarshal result: %s: %w", string(resp.Answer.Result), err) - } - - return result.Records.TXT, nil -} - -// ChangeTXTRecord changes TXT records. -// https://beget.com/ru/kb/api/funkczii-upravleniya-dns#changerecords -func (c *Client) ChangeTXTRecord(ctx context.Context, domain string, records []Record) error { - request := ChangeRecordsRequest{ - Fqdn: domain, - Records: RecordList{TXT: records}, - } - - resp, err := c.doRequest(ctx, request, "dns", "changeRecords") - if err != nil { - return err - } - - return resp.HasError() -} - -func (c *Client) doRequest(ctx context.Context, data any, fragments ...string) (*APIResponse, error) { - endpoint := c.BaseURL.JoinPath(fragments...) - - inputData, err := json.Marshal(data) - if err != nil { - return nil, fmt.Errorf("failed to mashall input data: %w", err) - } - - query := endpoint.Query() - query.Add("input_data", string(inputData)) - query.Add("login", c.login) - query.Add("passwd", c.password) - query.Add("input_format", "json") - query.Add("output_format", "json") - endpoint.RawQuery = query.Encode() - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return nil, errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return nil, parseError(req, resp) - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return nil, errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - var apiResp APIResponse - - err = json.Unmarshal(raw, &apiResp) - if err != nil { - return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return &apiResp, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var apiResp APIResponse - - err := json.Unmarshal(raw, &apiResp) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return fmt.Errorf("[status code %d] %w", resp.StatusCode, apiResp) -} diff --git a/providers/dns/beget/internal/client_test.go b/providers/dns/beget/internal/client_test.go deleted file mode 100644 index 4c127abf1..000000000 --- a/providers/dns/beget/internal/client_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package internal - -import ( - "context" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("user", "secret") - - client.HTTPClient = server.Client() - client.BaseURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckQueryParameter(). - With("login", "user"). - With("passwd", "secret"). - With("input_format", "json"). - With("output_format", "json"), - ) -} - -func TestClient_GetTXTRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/getData", - servermock.ResponseFromFixture("getData-real.json"), - servermock.CheckQueryParameter(). - With("input_data", `{"fqdn":"example.com"}`), - ). - Build(t) - - data, err := client.GetTXTRecords(context.Background(), "example.com") - require.NoError(t, err) - - expected := []Record{{Data: "v=spf1 redirect=beget.com", TTL: 300}} - - assert.Equal(t, expected, data) -} - -func TestClient_ChangeTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/changeRecords", - servermock.ResponseFromFixture("changeRecords-doc.json"), - servermock.CheckQueryParameter(). - With("input_data", `{"fqdn":"sub.example.com","records":{"TXT":[{"value":"txtTXTtxt","priority":10,"ttl":300}]}}`), - ). - Build(t) - - records := []Record{{Value: "txtTXTtxt", TTL: 300, Priority: 10}} - - err := client.ChangeTXTRecord(context.Background(), "sub.example.com", records) - require.NoError(t, err) -} - -func TestClient_ChangeTXTRecord_error(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/changeRecords", - servermock.ResponseFromFixture("error.json")). - Build(t) - - records := []Record{{Data: "txtTXTtxt", TTL: 300}} - - err := client.ChangeTXTRecord(context.Background(), "sub.example.com", records) - require.Error(t, err) - - require.EqualError(t, err, "API error: NO_SUCH_METHOD: No such method") -} - -func TestClient_ChangeTXTRecord_answer_error(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/changeRecords", - servermock.ResponseFromFixture("answer_error.json")). - Build(t) - - records := []Record{{Data: "txtTXTtxt", TTL: 300}} - - err := client.ChangeTXTRecord(context.Background(), "sub.example.com", records) - require.Error(t, err) - - require.EqualError(t, err, "API answer error: INVALID_DATA: Login length cannot be greater than 12 characters") -} - -func TestClient_ChangeTXTRecord_remove(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/changeRecords", - servermock.ResponseFromFixture("changeRecords-doc.json"), - servermock.CheckQueryParameter(). - With("input_data", `{"fqdn":"sub.example.com","records":{}}`), - ). - Build(t) - - err := client.ChangeTXTRecord(context.Background(), "sub.example.com", nil) - require.NoError(t, err) -} diff --git a/providers/dns/beget/internal/fixtures/answer_error.json b/providers/dns/beget/internal/fixtures/answer_error.json deleted file mode 100644 index 12f5fdda7..000000000 --- a/providers/dns/beget/internal/fixtures/answer_error.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "status": "success", - "answer": { - "status": "error", - "errors": [ - { - "error_code": "INVALID_DATA", - "error_text": "Login length cannot be greater than 12 characters" - } - ] - } -} diff --git a/providers/dns/beget/internal/fixtures/changeRecords-doc.json b/providers/dns/beget/internal/fixtures/changeRecords-doc.json deleted file mode 100644 index 4c182d4e6..000000000 --- a/providers/dns/beget/internal/fixtures/changeRecords-doc.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "status": "success", - "answer": { - "status": "success", - "result": { - "A": [ - { - "priority": 10, - "value": "127.0.0.1" - } - ], - "MX": [ - { - "priority": 10, - "value": "mx1.beget.ru" - }, - { - "priority": 20, - "value": "mx2.beget.ru" - } - ], - "TXT": [ - { - "priority": 10, - "value": "TXT record" - } - ] - } - } -} - diff --git a/providers/dns/beget/internal/fixtures/error.json b/providers/dns/beget/internal/fixtures/error.json deleted file mode 100644 index 1dd2a111e..000000000 --- a/providers/dns/beget/internal/fixtures/error.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "status": "error", - "error_text": "No such method", - "error_code": "NO_SUCH_METHOD" -} diff --git a/providers/dns/beget/internal/fixtures/getData-doc.json b/providers/dns/beget/internal/fixtures/getData-doc.json deleted file mode 100644 index bed5b7461..000000000 --- a/providers/dns/beget/internal/fixtures/getData-doc.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "status": "success", - "answer": { - "status": "success", - "result": { - "is_under_control": 1, - "is_beget_dns": 1, - "is_subdomain": 0, - "fqdn": "beget.ru", - "records": { - "DNS": [ - { - "value": "ns1.beget.ru", - "priority": 10 - }, - { - "value": "ns2.beget.ru", - "priority": 20 - } - ], - "DNS_IP": [ - { - "value": null, - "priority": 10 - }, - { - "value": null, - "priority": 20 - } - ], - "A": [ - { - "value": "91.106.201.65", - "priority": "0" - } - ], - "MX": [ - { - "value": "mx1.beget.ru", - "priority": "10" - }, - { - "value": "mx2.beget.ru", - "priority": "20" - } - ], - "TXT": [ - { - "value": "", - "priority": 0 - } - ] - }, - "set_type": 1 - } - } -} - diff --git a/providers/dns/beget/internal/fixtures/getData-real.json b/providers/dns/beget/internal/fixtures/getData-real.json deleted file mode 100644 index 700c756e8..000000000 --- a/providers/dns/beget/internal/fixtures/getData-real.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "status": "success", - "answer": { - "status": "success", - "result": { - "is_under_control": true, - "is_beget_dns": true, - "is_subdomain": false, - "fqdn": "example.com", - "records": { - "MX": [ - { - "ttl": 300, - "exchange": "mx2.beget.com.", - "preference": 20 - }, - { - "ttl": 300, - "exchange": "mx1.beget.com.", - "preference": 10 - } - ], - "TXT": [ - { - "ttl": 300, - "txtdata": "v=spf1 redirect=beget.com" - } - ], - "A": [ - { - "ttl": 300, - "address": "1.2.3.4" - } - ], - "DNS": [ - { - "value": "ns1.beget.pro" - }, - { - "value": "ns2.beget.pro" - }, - { - "value": "ns1.beget.com" - }, - { - "value": "ns2.beget.com" - } - ], - "DNS_IP": [ - { - "value": "" - }, - { - "value": "" - }, - { - "value": "" - }, - { - "value": "" - } - ] - }, - "set_type": 1 - } - } -} diff --git a/providers/dns/beget/internal/fixtures/getData.json b/providers/dns/beget/internal/fixtures/getData.json deleted file mode 100644 index 571b6ac31..000000000 --- a/providers/dns/beget/internal/fixtures/getData.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "status": "success", - "answer": { - "status": "success", - "result": { - "is_under_control": true, - "is_beget_dns": true, - "is_subdomain": false, - "fqdn": "_acme-challenge.example.com", - "records": { - "MX": [ - { - "ttl": 300, - "exchange": "mx2.beget.com.", - "preference": 20 - }, - { - "ttl": 300, - "exchange": "mx1.beget.com.", - "preference": 10 - } - ], - "TXT": [ - { - "ttl": 300, - "txtdata": "foo" - } - ], - "A": [ - { - "ttl": 300, - "address": "1.2.3.4" - } - ], - "DNS": [ - { - "value": "ns1.beget.pro" - }, - { - "value": "ns2.beget.pro" - }, - { - "value": "ns1.beget.com" - }, - { - "value": "ns2.beget.com" - } - ], - "DNS_IP": [ - { - "value": "" - }, - { - "value": "" - }, - { - "value": "" - }, - { - "value": "" - } - ] - }, - "set_type": 1 - } - } -} diff --git a/providers/dns/beget/internal/fixtures/getData_empty.json b/providers/dns/beget/internal/fixtures/getData_empty.json deleted file mode 100644 index ea819eeca..000000000 --- a/providers/dns/beget/internal/fixtures/getData_empty.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "status": "success", - "answer": { - "status": "success", - "result": { - "is_under_control": true, - "is_beget_dns": true, - "is_subdomain": false, - "fqdn": "_acme-challenge.example.com", - "set_type": 1 - } - } -} diff --git a/providers/dns/beget/internal/types.go b/providers/dns/beget/internal/types.go deleted file mode 100644 index f453bf628..000000000 --- a/providers/dns/beget/internal/types.go +++ /dev/null @@ -1,100 +0,0 @@ -package internal - -import ( - "encoding/json" - "fmt" - "strings" -) - -const successResult = "success" - -// APIResponse is the representation of an API response. -type APIResponse struct { - Status string `json:"status"` - - Answer *Answer `json:"answer,omitempty"` - - ErrorCode string `json:"error_code,omitempty"` - ErrorText string `json:"error_text,omitempty"` -} - -func (a APIResponse) Error() string { - return fmt.Sprintf("API %s: %s: %s", a.Status, a.ErrorCode, a.ErrorText) -} - -// HasError returns an error is the response contains an error. -func (a APIResponse) HasError() error { - if a.Status != successResult { - return a - } - - if a.Answer == nil || a.Status != successResult || a.Answer.Status != successResult { - return a.Answer - } - - return nil -} - -// Answer is the representation of an API response answer. -type Answer struct { - Status string `json:"status,omitempty"` - Result json.RawMessage `json:"result,omitempty"` - - Errors []AnswerError `json:"errors,omitempty"` - ErrorCode string `json:"error_code,omitempty"` - ErrorText string `json:"error_text,omitempty"` -} - -type AnswerError struct { - ErrorCode string `json:"error_code,omitempty"` - ErrorText string `json:"error_text,omitempty"` -} - -func (a Answer) Error() string { - parts := []string{fmt.Sprintf("API answer %s", a.Status)} - - if a.ErrorCode != "" { - parts = append(parts, a.ErrorCode) - } - - if a.ErrorText != "" { - parts = append(parts, a.ErrorText) - } - - if len(a.Errors) > 0 { - for _, e := range a.Errors { - parts = append(parts, e.ErrorCode, e.ErrorText) - } - } - - return strings.Join(parts, ": ") -} - -// GetRecordsRequest data representation for data get request. -type GetRecordsRequest struct { - Fqdn string `json:"fqdn,omitempty"` -} - -// ChangeRecordsRequest data representation for data change request. -type ChangeRecordsRequest struct { - Fqdn string `json:"fqdn,omitempty"` - Records RecordList `json:"records"` -} - -// RecordList List of entries (in this case only described TXT). -type RecordList struct { - TXT []Record `json:"TXT,omitempty"` -} - -// Record data representation for TXT record. -type Record struct { - Value string `json:"value,omitempty"` - Data string `json:"txtdata,omitempty"` - Priority int `json:"priority,omitempty"` - TTL int `json:"ttl,omitempty"` -} - -type GetRecordsResult struct { - Fqdn string `json:"fqdn"` - Records RecordList `json:"records"` -} diff --git a/providers/dns/binarylane/binarylane.go b/providers/dns/binarylane/binarylane.go deleted file mode 100644 index 5bbb7a16a..000000000 --- a/providers/dns/binarylane/binarylane.go +++ /dev/null @@ -1,165 +0,0 @@ -// Package binarylane implements a DNS provider for solving the DNS-01 challenge using Binary Lane. -package binarylane - -import ( - "context" - "errors" - "fmt" - "net/http" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/binarylane/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "BINARYLANE_" - - EnvAPIToken = envNamespace + "API_TOKEN" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIToken string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, 3600), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client - - recordIDs map[string]int64 - recordIDsMu sync.Mutex -} - -// NewDNSProvider returns a DNSProvider instance configured for Binary Lane. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIToken) - if err != nil { - return nil, fmt.Errorf("binarylane: %w", err) - } - - config := NewDefaultConfig() - config.APIToken = values[EnvAPIToken] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Binary Lane. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("binarylane: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.APIToken) - if err != nil { - return nil, fmt.Errorf("binarylane: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - recordIDs: make(map[string]int64), - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("binarylane: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("binarylane: %w", err) - } - - record := internal.Record{ - Type: "TXT", - Name: subDomain, - Data: info.Value, - TTL: d.config.TTL, - } - - response, err := d.client.CreateRecord(context.Background(), dns01.UnFqdn(authZone), record) - if err != nil { - return fmt.Errorf("binarylane: create record: %w", err) - } - - d.recordIDsMu.Lock() - d.recordIDs[token] = response.ID - d.recordIDsMu.Unlock() - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("binarylane: could not find zone for domain %q: %w", domain, err) - } - - // get the record's unique ID from when we created it - d.recordIDsMu.Lock() - recordID, ok := d.recordIDs[token] - d.recordIDsMu.Unlock() - - if !ok { - return fmt.Errorf("binarylane: unknown record ID for '%s'", info.EffectiveFQDN) - } - - err = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), recordID) - if err != nil { - return fmt.Errorf("binarylane: delete record: %w", err) - } - - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/binarylane/binarylane.toml b/providers/dns/binarylane/binarylane.toml deleted file mode 100644 index 8b382f3b2..000000000 --- a/providers/dns/binarylane/binarylane.toml +++ /dev/null @@ -1,22 +0,0 @@ -Name = "Binary Lane" -Description = '''''' -URL = "https://www.binarylane.com.au/" -Code = "binarylane" -Since = "v4.26.0" - -Example = ''' -BINARYLANE_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns binarylane -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - BINARYLANE_API_TOKEN = "API token" - [Configuration.Additional] - BINARYLANE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - BINARYLANE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - BINARYLANE_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - BINARYLANE_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://api.binarylane.com.au/reference/#tag/Domains" diff --git a/providers/dns/binarylane/binarylane_test.go b/providers/dns/binarylane/binarylane_test.go deleted file mode 100644 index 4f2cfd230..000000000 --- a/providers/dns/binarylane/binarylane_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package binarylane - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAPIToken).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAPIToken: "secret", - }, - }, - { - desc: "missing API token", - envVars: map[string]string{ - EnvAPIToken: "", - }, - expected: "binarylane: some credentials information are missing: BINARYLANE_API_TOKEN", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiToken string - expected string - }{ - { - desc: "success", - apiToken: "secret", - }, - { - desc: "missing API token", - expected: "binarylane: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIToken = test.apiToken - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/binarylane/internal/client.go b/providers/dns/binarylane/internal/client.go deleted file mode 100644 index 3f10e9f8b..000000000 --- a/providers/dns/binarylane/internal/client.go +++ /dev/null @@ -1,148 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" -) - -const defaultBaseURL = "https://api.binarylane.com.au/v2/" - -const authorizationHeader = "Authorization" - -// Client the Binary Lane API client. -type Client struct { - apiToken string - - baseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(apiToken string) (*Client, error) { - if apiToken == "" { - return nil, errors.New("credentials missing") - } - - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - apiToken: apiToken, - baseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -// CreateRecord Creates a new domain record. -// https://api.binarylane.com.au/reference/#tag/Domains/paths/~1v2~1domains~1%7Bdomain_name%7D~1records/post -func (c *Client) CreateRecord(ctx context.Context, domain string, record Record) (*Record, error) { - endpoint := c.baseURL.JoinPath("domains", domain, "records") - - if record.Name == "" { - record.Name = "@" - } - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) - if err != nil { - return nil, err - } - - var result APIResponse - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return result.DomainRecord, nil -} - -// DeleteRecord Deletes an existing domain record. -// https://api.binarylane.com.au/reference/#tag/Domains/paths/~1v2~1domains~1%7Bdomain_name%7D~1records~1%7Brecord_id%7D/delete -func (c *Client) DeleteRecord(ctx context.Context, domainName string, recordID int64) error { - endpoint := c.baseURL.JoinPath("domains", domainName, "records", strconv.FormatInt(recordID, 10)) - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return err - } - - return c.do(req, nil) -} - -func (c *Client) do(req *http.Request, result any) error { - req.Header.Set(authorizationHeader, "Bearer "+c.apiToken) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIError - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return &errAPI -} diff --git a/providers/dns/binarylane/internal/client_test.go b/providers/dns/binarylane/internal/client_test.go deleted file mode 100644 index 0398d5adf..000000000 --- a/providers/dns/binarylane/internal/client_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("secret") - if err != nil { - return nil, err - } - - client.baseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("Bearer secret"), - ) -} - -func TestClient_CreateRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/example.com/records", - servermock.ResponseFromFixture("create_record.json"), - servermock.CheckRequestJSONBodyFromFixture("create_record-request.json")). - Build(t) - - record := Record{ - Type: "TXT", - Name: "foo", - Data: "txtTXTtxt", - TTL: 300, - } - - rec, err := client.CreateRecord(t.Context(), "example.com", record) - require.NoError(t, err) - - expected := &Record{ - ID: 123, - Type: "TXT", - Name: "foo", - Data: "txtTXTtxt", - TTL: 300, - } - - require.Equal(t, expected, rec) -} - -func TestClient_CreateRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/example.com/records", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) - - record := Record{ - Type: "TXT", - Name: "foo", - Data: "txtTXTtxt", - TTL: 300, - } - - _, err := client.CreateRecord(t.Context(), "example.com", record) - require.EqualError(t, err, "400: type: title: detail: instance: property1: a") -} - -func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /domains/example.com/records/123", - servermock.Noop(). - WithStatusCode(http.StatusNoContent)). - Build(t) - - err := client.DeleteRecord(t.Context(), "example.com", 123) - require.NoError(t, err) -} - -func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /domains/example.com/records/123", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) - - err := client.DeleteRecord(t.Context(), "example.com", 123) - require.EqualError(t, err, "400: type: title: detail: instance: property1: a") -} diff --git a/providers/dns/binarylane/internal/fixtures/create_record-request.json b/providers/dns/binarylane/internal/fixtures/create_record-request.json deleted file mode 100644 index 98a349650..000000000 --- a/providers/dns/binarylane/internal/fixtures/create_record-request.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "type": "TXT", - "name": "foo", - "data": "txtTXTtxt", - "ttl": 300 -} diff --git a/providers/dns/binarylane/internal/fixtures/create_record.json b/providers/dns/binarylane/internal/fixtures/create_record.json deleted file mode 100644 index 709bef23e..000000000 --- a/providers/dns/binarylane/internal/fixtures/create_record.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain_record": { - "id": 123, - "type": "TXT", - "name": "foo", - "data": "txtTXTtxt", - "ttl": 300 - } -} diff --git a/providers/dns/binarylane/internal/fixtures/error.json b/providers/dns/binarylane/internal/fixtures/error.json deleted file mode 100644 index 79d115f74..000000000 --- a/providers/dns/binarylane/internal/fixtures/error.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "type", - "title": "title", - "status": 400, - "detail": "detail", - "instance": "instance", - "errors": { - "property1": [ - "a" - ] - }, - "property1": null, - "property2": null -} diff --git a/providers/dns/binarylane/internal/types.go b/providers/dns/binarylane/internal/types.go deleted file mode 100644 index 06d4be5c0..000000000 --- a/providers/dns/binarylane/internal/types.go +++ /dev/null @@ -1,44 +0,0 @@ -package internal - -import ( - "fmt" - "strings" -) - -type APIError struct { - Type string `json:"type"` - Title string `json:"title"` - Status int `json:"status"` - Detail string `json:"detail"` - Instance string `json:"instance"` - Errors map[string][]string `json:"errors"` -} - -func (a *APIError) Error() string { - msg := new(strings.Builder) - - _, _ = fmt.Fprintf(msg, "%d: %s: %s: %s: %s", a.Status, a.Type, a.Title, a.Detail, a.Instance) - - for s, values := range a.Errors { - _, _ = fmt.Fprintf(msg, ": %s: %s", s, strings.Join(values, ", ")) - } - - return msg.String() -} - -type Record struct { - ID int64 `json:"id,omitempty"` - Type string `json:"type,omitempty"` - Name string `json:"name,omitempty"` - Data string `json:"data,omitempty"` - Priority int `json:"priority,omitempty"` - Port int `json:"port,omitempty"` - TTL int `json:"ttl,omitempty"` - Weight int `json:"weight,omitempty"` - Flags int `json:"flags,omitempty"` - Tag string `json:"tag,omitempty"` -} - -type APIResponse struct { - DomainRecord *Record `json:"domain_record"` -} diff --git a/providers/dns/bindman/bindman.go b/providers/dns/bindman/bindman.go index c529cb63c..1ec396075 100644 --- a/providers/dns/bindman/bindman.go +++ b/providers/dns/bindman/bindman.go @@ -7,11 +7,9 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - bindman "github.com/labbsr0x/bindman-dns-webhook/src/client" + "github.com/labbsr0x/bindman-dns-webhook/src/client" ) // Environment variables names. @@ -25,8 +23,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { PropagationTimeout time.Duration @@ -49,7 +45,7 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config - client *bindman.DNSWebhookClient + client *client.DNSWebhookClient } // NewDNSProvider returns a DNSProvider instance configured for Bindman. @@ -76,17 +72,12 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("bindman: bindman manager address missing") } - // Because the client.New uses the http.DefaultClient. - if config.HTTPClient == nil { - config.HTTPClient = &http.Client{Timeout: time.Minute} - } - - client, err := bindman.New(config.BaseURL, clientdebug.Wrap(config.HTTPClient)) + bClient, err := client.New(config.BaseURL, config.HTTPClient) if err != nil { return nil, fmt.Errorf("bindman: %w", err) } - return &DNSProvider{config: config, client: client}, nil + return &DNSProvider{config: config, client: bClient}, nil } // Present creates a TXT record using the specified parameters. @@ -98,7 +89,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { if err := d.client.AddRecord(info.EffectiveFQDN, "TXT", info.Value); err != nil { return fmt.Errorf("bindman: %w", err) } - return nil } @@ -109,7 +99,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { if err := d.client.RemoveRecord(info.EffectiveFQDN, "TXT"); err != nil { return fmt.Errorf("bindman: %w", err) } - return nil } diff --git a/providers/dns/bindman/bindman.toml b/providers/dns/bindman/bindman.toml index 768601588..9804bf62d 100644 --- a/providers/dns/bindman/bindman.toml +++ b/providers/dns/bindman/bindman.toml @@ -6,16 +6,16 @@ Since = "v2.6.0" Example = ''' BINDMAN_MANAGER_ADDRESS= \ -lego --dns bindman -d '*.example.com' -d example.com run +lego --email you@example.com --dns bindman --domains my.example.org run ''' [Configuration] [Configuration.Credentials] BINDMAN_MANAGER_ADDRESS = "The server URL, should have scheme, hostname, and port (if required) of the Bindman-DNS Manager server" [Configuration.Additional] - BINDMAN_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - BINDMAN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - BINDMAN_HTTP_TIMEOUT = "API request timeout in seconds (Default: 60)" + BINDMAN_POLLING_INTERVAL = "Time between DNS propagation check" + BINDMAN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + BINDMAN_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://gitlab.isc.org/isc-projects/bind9" diff --git a/providers/dns/bindman/bindman_test.go b/providers/dns/bindman/bindman_test.go index 978a1d006..a0db025e7 100644 --- a/providers/dns/bindman/bindman_test.go +++ b/providers/dns/bindman/bindman_test.go @@ -1,13 +1,14 @@ +// Package bindman implements a DNS provider for solving the DNS-01 challenge. package bindman import ( + "errors" "net/http" - "net/http/httptest" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" + bindmanClient "github.com/labbsr0x/bindman-dns-webhook/src/client" "github.com/stretchr/testify/require" ) @@ -46,7 +47,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -106,24 +106,10 @@ func TestNewDNSProviderConfig(t *testing.T) { } } -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.BaseURL = server.URL - config.HTTPClient = server.Client() - - return NewDNSProviderConfig(config) - }, - servermock.CheckHeader(). - WithJSONHeaders(). - With("User-Agent", "bindman-dns-webhook-client")) -} - func TestDNSProvider_Present(t *testing.T) { testCases := []struct { name string - mock *servermock.Builder[*DNSProvider] + client *bindmanClient.DNSWebhookClient domain string token string keyAuth string @@ -131,31 +117,28 @@ func TestDNSProvider_Present(t *testing.T) { }{ { name: "success when add record function return no error", - mock: mockBuilder(). - Route("POST /records", - servermock.Noop().WithStatusCode(http.StatusNoContent), - servermock.CheckRequestJSONBodyFromFixture("add_record-request.json"), - ), - domain: "example.com", + client: &bindmanClient.DNSWebhookClient{ + ClientAPI: &MockHTTPClientAPI{Status: http.StatusNoContent}, + }, + domain: "hello.test.com", keyAuth: "szDTG4zmM0GsKG91QAGO2M4UYOJMwU8oFpWOP7eTjCw", expectError: false, }, { name: "error when add record function return an error", - mock: mockBuilder(). - Route("POST /records", - servermock.ResponseFromFixture("error.json"), - ), - domain: "example.com", + client: &bindmanClient.DNSWebhookClient{ + ClientAPI: &MockHTTPClientAPI{Error: errors.New("error adding record")}, + }, + domain: "hello.test.com", keyAuth: "szDTG4zmM0GsKG91QAGO2M4UYOJMwU8oFpWOP7eTjCw", expectError: true, }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { - provider := test.mock.Build(t) + d := &DNSProvider{client: test.client} - err := provider.Present(test.domain, test.token, test.keyAuth) + err := d.Present(test.domain, test.token, test.keyAuth) if test.expectError { require.Error(t, err) } else { @@ -168,7 +151,7 @@ func TestDNSProvider_Present(t *testing.T) { func TestDNSProvider_CleanUp(t *testing.T) { testCases := []struct { name string - mock *servermock.Builder[*DNSProvider] + client *bindmanClient.DNSWebhookClient domain string token string keyAuth string @@ -176,33 +159,30 @@ func TestDNSProvider_CleanUp(t *testing.T) { }{ { name: "success when remove record function return no error", - mock: mockBuilder(). - Route("DELETE /records/_acme-challenge.example.com./TXT", - servermock.Noop().WithStatusCode(http.StatusNoContent), - ), - domain: "example.com", + client: &bindmanClient.DNSWebhookClient{ + ClientAPI: &MockHTTPClientAPI{Status: http.StatusNoContent}, + }, + domain: "hello.test.com", keyAuth: "szDTG4zmM0GsKG91QAGO2M4UYOJMwU8oFpWOP7eTjCw", expectError: false, }, { name: "error when remove record function return an error", - mock: mockBuilder(). - Route("DELETE /records/_acme-challenge.example.com./TXT", - servermock.ResponseFromFixture("error.json"), - ), - domain: "example.com", + client: &bindmanClient.DNSWebhookClient{ + ClientAPI: &MockHTTPClientAPI{Error: errors.New("error adding record")}, + }, + domain: "hello.test.com", keyAuth: "szDTG4zmM0GsKG91QAGO2M4UYOJMwU8oFpWOP7eTjCw", expectError: true, }, } - for _, test := range testCases { t.Run(test.name, func(t *testing.T) { - provider := test.mock.Build(t) + d := &DNSProvider{client: test.client} - err := provider.CleanUp(test.domain, test.token, test.keyAuth) + err := d.CleanUp(test.domain, test.token, test.keyAuth) if test.expectError { - require.ErrorContains(t, err, "bindman: ERROR (400): bar; ") + require.Error(t, err) } else { require.NoError(t, err) } @@ -216,7 +196,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -230,7 +209,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -239,3 +217,25 @@ func TestLiveCleanUp(t *testing.T) { err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } + +type MockHTTPClientAPI struct { + Data []byte + Status int + Error error +} + +func (m *MockHTTPClientAPI) Put(url string, data []byte) (*http.Response, []byte, error) { + return &http.Response{StatusCode: m.Status}, m.Data, m.Error +} + +func (m *MockHTTPClientAPI) Post(url string, data []byte) (*http.Response, []byte, error) { + return &http.Response{StatusCode: m.Status}, m.Data, m.Error +} + +func (m *MockHTTPClientAPI) Get(url string) (*http.Response, []byte, error) { + return &http.Response{StatusCode: m.Status}, m.Data, m.Error +} + +func (m *MockHTTPClientAPI) Delete(url string) (*http.Response, []byte, error) { + return &http.Response{StatusCode: m.Status}, m.Data, m.Error +} diff --git a/providers/dns/bindman/fixtures/add_record-request.json b/providers/dns/bindman/fixtures/add_record-request.json deleted file mode 100644 index 9585565b8..000000000 --- a/providers/dns/bindman/fixtures/add_record-request.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "_acme-challenge.example.com.", - "value": "_EYMkjukXEMcXbnvpT6WLESzfYhxH190NKTBo3cpu-E", - "type": "TXT" -} diff --git a/providers/dns/bindman/fixtures/error.json b/providers/dns/bindman/fixtures/error.json deleted file mode 100644 index c8a014510..000000000 --- a/providers/dns/bindman/fixtures/error.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "message": "bar", - "code": 400, - "details": ["foo"] -} diff --git a/providers/dns/bluecat/bluecat.go b/providers/dns/bluecat/bluecat.go index b26fab8be..58ac2147b 100644 --- a/providers/dns/bluecat/bluecat.go +++ b/providers/dns/bluecat/bluecat.go @@ -8,12 +8,10 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/bluecat/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -26,7 +24,6 @@ const ( EnvConfigName = envNamespace + "CONFIG_NAME" EnvDNSView = envNamespace + "DNS_VIEW" EnvDebug = envNamespace + "DEBUG" - EnvSkipDeploy = envNamespace + "SKIP_DEPLOY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" @@ -34,8 +31,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string @@ -48,7 +43,6 @@ type Config struct { TTL int HTTPClient *http.Client Debug bool - SkipDeploy bool } // NewDefaultConfig returns a default configuration for the DNSProvider. @@ -60,8 +54,7 @@ func NewDefaultConfig() *Config { HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, - Debug: env.GetOrDefaultBool(EnvDebug, false), - SkipDeploy: env.GetOrDefaultBool(EnvSkipDeploy, false), + Debug: env.GetOrDefaultBool(EnvDebug, false), } } @@ -111,8 +104,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{config: config, client: client}, nil } @@ -152,11 +143,9 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("bluecat: add TXT record: %w", err) } - if !d.config.SkipDeploy { - err = d.client.Deploy(ctx, parentZoneID) - if err != nil { - return fmt.Errorf("bluecat: deploy: %w", err) - } + err = d.client.Deploy(ctx, parentZoneID) + if err != nil { + return fmt.Errorf("bluecat: deploy: %w", err) } err = d.client.Logout(ctx) @@ -196,11 +185,9 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("bluecat: delete TXT record: %w", err) } - if !d.config.SkipDeploy { - err = d.client.Deploy(ctx, parentZoneID) - if err != nil { - return fmt.Errorf("bluecat: deploy: %w", err) - } + err = d.client.Deploy(ctx, parentZoneID) + if err != nil { + return fmt.Errorf("bluecat: deploy: %w", err) } err = d.client.Logout(ctx) diff --git a/providers/dns/bluecat/bluecat.toml b/providers/dns/bluecat/bluecat.toml index 15df6ed34..f1094aec3 100644 --- a/providers/dns/bluecat/bluecat.toml +++ b/providers/dns/bluecat/bluecat.toml @@ -11,7 +11,7 @@ BLUECAT_USER_NAME=myusername \ BLUECAT_CONFIG_NAME=myconfig \ BLUECAT_SERVER_URL=https://bam.example.com \ BLUECAT_TTL=30 \ -lego --dns bluecat -d '*.example.com' -d example.com run +lego --email you@example.com --dns bluecat --domains my.example.org run ''' [Configuration] @@ -22,11 +22,10 @@ lego --dns bluecat -d '*.example.com' -d example.com run BLUECAT_CONFIG_NAME = "Configuration name" BLUECAT_DNS_VIEW = "External DNS View Name" [Configuration.Additional] - BLUECAT_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - BLUECAT_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - BLUECAT_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - BLUECAT_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - BLUECAT_SKIP_DEPLOY = "Skip deployements" + BLUECAT_POLLING_INTERVAL = "Time between DNS propagation check" + BLUECAT_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + BLUECAT_TTL = "The TTL of the TXT record used for the DNS challenge" + BLUECAT_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/REST-API/9.1.0" diff --git a/providers/dns/bluecat/bluecat_test.go b/providers/dns/bluecat/bluecat_test.go index 38b110470..5a3670e3a 100644 --- a/providers/dns/bluecat/bluecat_test.go +++ b/providers/dns/bluecat/bluecat_test.go @@ -105,7 +105,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -220,7 +219,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -234,7 +232,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/bluecat/internal/client.go b/providers/dns/bluecat/internal/client.go index d517ea857..a2649a455 100644 --- a/providers/dns/bluecat/internal/client.go +++ b/providers/dns/bluecat/internal/client.go @@ -36,7 +36,7 @@ type Client struct { HTTPClient *http.Client } -func NewClient(baseURL, username, password string) *Client { +func NewClient(baseURL string, username, password string) *Client { bu, _ := url.Parse(baseURL) return &Client{ @@ -106,7 +106,6 @@ func (c *Client) AddEntity(ctx context.Context, parentID uint, entity Entity) (u // addEntity responds only with body text containing the ID of the created record addTxtResp := string(raw) - id, err := strconv.ParseUint(addTxtResp, 10, 64) if err != nil { return 0, fmt.Errorf("addEntity request failed: %s", addTxtResp) @@ -148,7 +147,6 @@ func (c *Client) GetEntityByName(ctx context.Context, parentID uint, name, objTy } var entity EntityResponse - err = json.Unmarshal(raw, &entity) if err != nil { return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) diff --git a/providers/dns/bluecat/internal/client_test.go b/providers/dns/bluecat/internal/client_test.go index d4776b8a1..206d7d1a4 100644 --- a/providers/dns/bluecat/internal/client_test.go +++ b/providers/dns/bluecat/internal/client_test.go @@ -1,45 +1,41 @@ package internal import ( + "context" "encoding/json" "net/http" "net/http/httptest" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func setupClient(server *httptest.Server) (*Client, error) { +func TestClient_LookupParentZoneID(t *testing.T) { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + client := NewClient(server.URL, "user", "secret") client.HTTPClient = server.Client() - return client, nil -} + mux.HandleFunc("/Services/REST/v1/getEntityByName", func(rw http.ResponseWriter, req *http.Request) { + query := req.URL.Query() -func TestClient_LookupParentZoneID(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /Services/REST/v1/getEntityByName", - http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - query := req.URL.Query() + if query.Get("name") == "com" { + _ = json.NewEncoder(rw).Encode(EntityResponse{ + ID: 2, + Name: "com", + Type: ZoneType, + Properties: "test", + }) + return + } - if query.Get("name") == "com" { - _ = json.NewEncoder(rw).Encode(EntityResponse{ - ID: 2, - Name: "com", - Type: ZoneType, - Properties: "test", - }) + http.Error(rw, "{}", http.StatusOK) + }) - return - } - - _, _ = rw.Write([]byte(`{}`)) - })). - Build(t) - - parentID, name, err := client.LookupParentZoneID(t.Context(), 2, "foo.example.com") + parentID, name, err := client.LookupParentZoneID(context.Background(), 2, "foo.example.com") require.NoError(t, err) assert.EqualValues(t, 2, parentID) diff --git a/providers/dns/bluecat/internal/identity_test.go b/providers/dns/bluecat/internal/identity_test.go index 9ad4c18e6..378f6ab38 100644 --- a/providers/dns/bluecat/internal/identity_test.go +++ b/providers/dns/bluecat/internal/identity_test.go @@ -1,9 +1,12 @@ package internal import ( + "context" + "fmt" + "net/http" + "net/http/httptest" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -11,18 +14,41 @@ import ( const fakeToken = "BAMAuthToken: dQfuRMTUxNjc3MjcyNDg1ODppcGFybXM=" func TestClient_CreateAuthenticatedContext(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /Services/REST/v1/login", - servermock.RawStringResponse(fakeToken), - servermock.CheckQueryParameter(). - With("username", "user"). - With("password", "secret")). - Route("DELETE /Services/REST/v1/delete", nil, - servermock.CheckHeader(). - WithAuthorization(fakeToken)). - Build(t) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) - ctx, err := client.CreateAuthenticatedContext(t.Context()) + client := NewClient(server.URL, "user", "secret") + client.HTTPClient = server.Client() + + mux.HandleFunc("/Services/REST/v1/login", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + query := req.URL.Query() + if query.Get("username") != "user" { + http.Error(rw, fmt.Sprintf("invalid username %s", query.Get("username")), http.StatusUnauthorized) + return + } + + if query.Get("password") != "secret" { + http.Error(rw, fmt.Sprintf("invalid password %s", query.Get("password")), http.StatusUnauthorized) + return + } + + _, _ = fmt.Fprint(rw, fakeToken) + }) + mux.HandleFunc("/Services/REST/v1/delete", func(rw http.ResponseWriter, req *http.Request) { + authorization := req.Header.Get(authorizationHeader) + if authorization != fakeToken { + http.Error(rw, fmt.Sprintf("invalid credential: %s", authorization), http.StatusUnauthorized) + return + } + }) + + ctx, err := client.CreateAuthenticatedContext(context.Background()) require.NoError(t, err) at := getToken(ctx) diff --git a/providers/dns/bluecatv2/bluecatv2.go b/providers/dns/bluecatv2/bluecatv2.go deleted file mode 100644 index 0efe99661..000000000 --- a/providers/dns/bluecatv2/bluecatv2.go +++ /dev/null @@ -1,249 +0,0 @@ -// Package bluecatv2 implements a DNS provider for solving the DNS-01 challenge using Bluecat v2. -package bluecatv2 - -import ( - "context" - "errors" - "fmt" - "net/http" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/bluecatv2/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "BLUECATV2_" - - EnvServerURL = envNamespace + "SERVER_URL" - EnvUsername = envNamespace + "USERNAME" - EnvPassword = envNamespace + "PASSWORD" - EnvConfigName = envNamespace + "CONFIG_NAME" - EnvViewName = envNamespace + "VIEW_NAME" - EnvSkipDeploy = envNamespace + "SKIP_DEPLOY" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - ServerURL string - Username string - Password string - ConfigName string - ViewName string - SkipDeploy bool - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - SkipDeploy: env.GetOrDefaultBool(EnvSkipDeploy, false), - - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client - - zoneIDs map[string]int64 - recordIDs map[string]int64 - recordIDsMu sync.Mutex -} - -// NewDNSProvider returns a DNSProvider instance configured for Bluecat v2. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvServerURL, EnvUsername, EnvPassword, EnvConfigName, EnvViewName) - if err != nil { - return nil, fmt.Errorf("bluecatv2: %w", err) - } - - config := NewDefaultConfig() - config.ServerURL = values[EnvServerURL] - config.Username = values[EnvUsername] - config.Password = values[EnvPassword] - config.ConfigName = values[EnvConfigName] - config.ViewName = values[EnvViewName] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Bluecat v2. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("bluecatv2: the configuration of the DNS provider is nil") - } - - if config.ServerURL == "" { - return nil, errors.New("bluecatv2: missing server URL") - } - - if config.ConfigName == "" { - return nil, errors.New("bluecatv2: missing configuration name") - } - - if config.ViewName == "" { - return nil, errors.New("bluecatv2: missing view name") - } - - client, err := internal.NewClient(config.ServerURL, config.Username, config.Password) - if err != nil { - return nil, fmt.Errorf("bluecatv2: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - recordIDs: make(map[string]int64), - zoneIDs: make(map[string]int64), - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx, err := d.client.CreateAuthenticatedContext(context.Background()) - if err != nil { - return fmt.Errorf("bluecatv2: %w", err) - } - - zone, err := d.findZone(ctx, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("bluecatv2: %w", err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.AbsoluteName) - if err != nil { - return fmt.Errorf("bluecatv2: %w", err) - } - - record := internal.RecordTXT{ - CommonResource: internal.CommonResource{ - Type: "TXTRecord", - Name: subDomain, - }, - Text: info.Value, - TTL: d.config.TTL, - RecordType: "TXT", - } - - newRecord, err := d.client.CreateZoneResourceRecord(ctx, zone.ID, record) - if err != nil { - return fmt.Errorf("bluecatv2: create resource record: %w", err) - } - - d.recordIDsMu.Lock() - d.zoneIDs[token] = zone.ID - d.recordIDs[token] = newRecord.ID - d.recordIDsMu.Unlock() - - if d.config.SkipDeploy { - return nil - } - - _, err = d.client.CreateZoneDeployment(ctx, zone.ID) - if err != nil { - return fmt.Errorf("bluecat: deploy zone: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - d.recordIDsMu.Lock() - recordID, recordOK := d.recordIDs[token] - zoneID, zoneOK := d.zoneIDs[token] - d.recordIDsMu.Unlock() - - if !recordOK { - return fmt.Errorf("bluecatv2: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) - } - - if !zoneOK { - return fmt.Errorf("bluecatv2: unknown zone ID for '%s' '%s'", info.EffectiveFQDN, token) - } - - ctx, err := d.client.CreateAuthenticatedContext(context.Background()) - if err != nil { - return fmt.Errorf("bluecatv2: %w", err) - } - - err = d.client.DeleteResourceRecord(ctx, recordID) - if err != nil { - return fmt.Errorf("bluecatv2: delete resource record: %w", err) - } - - if d.config.SkipDeploy { - return nil - } - - _, err = d.client.CreateZoneDeployment(ctx, zoneID) - if err != nil { - return fmt.Errorf("bluecat: deploy zone: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -func (d *DNSProvider) findZone(ctx context.Context, fqdn string) (*internal.ZoneResource, error) { - for name := range dns01.UnFqdnDomainsSeq(fqdn) { - opts := &internal.CollectionOptions{ - Fields: "id,absoluteName,configuration.id,configuration.name,view.id,view.name", - Filter: internal.And( - internal.Eq("absoluteName", name), - internal.Eq("configuration.name", d.config.ConfigName), - internal.Eq("view.name", d.config.ViewName), - ).String(), - } - - zones, err := d.client.RetrieveZones(ctx, opts) - if err != nil { - // TODO(ldez) maybe add a log in v5. - continue - } - - for _, zone := range zones { - if zone.AbsoluteName == name { - return &zone, nil - } - } - } - - return nil, fmt.Errorf("no zone found for fqdn: %s", fqdn) -} diff --git a/providers/dns/bluecatv2/bluecatv2.toml b/providers/dns/bluecatv2/bluecatv2.toml deleted file mode 100644 index 6ec3781c6..000000000 --- a/providers/dns/bluecatv2/bluecatv2.toml +++ /dev/null @@ -1,33 +0,0 @@ -Name = "Bluecat v2" -Description = '''''' -URL = "https://www.bluecatnetworks.com" -Code = "bluecatv2" -Since = "v4.32.0" - -Example = ''' -BLUECATV2_SERVER_URL="https://example.com" \ -BLUECATV2_USERNAME="xxx" \ -BLUECATV2_PASSWORD="yyy" \ -BLUECATV2_CONFIG_NAME="myConfiguration" \ -BLUECATV2_VIEW_NAME="myView" \ -lego --dns bluecatv2 -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - BLUECAT_SERVER_URL = "The server URL: it should have a scheme, hostname, and port (if required) of the authoritative Bluecat BAM serve" - BLUECATV2_USERNAME = "API username" - BLUECATV2_PASSWORD = "API password" - BLUECATV2_CONFIG_NAME = "Configuration name" - BLUECATV2_VIEW_NAME = "DNS View Name" - [Configuration.Additional] - BLUECATV2_SKIP_DEPLOY = "Skip quick deployements" - BLUECATV2_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - BLUECATV2_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - BLUECATV2_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - BLUECATV2_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Introduction/9.6.0" - Swagger = "http://{Address_Manager_IP}/api/openapi.json" - SwaggerDump = "https://github.com/go-acme/lego/discussions/2218#discussioncomment-13060545" diff --git a/providers/dns/bluecatv2/bluecatv2_test.go b/providers/dns/bluecatv2/bluecatv2_test.go deleted file mode 100644 index d852f0e18..000000000 --- a/providers/dns/bluecatv2/bluecatv2_test.go +++ /dev/null @@ -1,414 +0,0 @@ -package bluecatv2 - -import ( - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/go-acme/lego/v4/providers/dns/bluecatv2/internal" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest( - EnvServerURL, - EnvUsername, - EnvPassword, - EnvConfigName, - EnvViewName, - EnvSkipDeploy, -).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvServerURL: "https://example.com/", - EnvUsername: "userA", - EnvPassword: "secret", - EnvConfigName: "myConfig", - EnvViewName: "myView", - }, - }, - { - desc: "missing server URL", - envVars: map[string]string{ - EnvServerURL: "", - EnvUsername: "userA", - EnvPassword: "secret", - EnvConfigName: "myConfig", - EnvViewName: "myView", - }, - expected: "bluecatv2: some credentials information are missing: BLUECATV2_SERVER_URL", - }, - { - desc: "missing username", - envVars: map[string]string{ - EnvServerURL: "https://example.com/", - EnvUsername: "", - EnvPassword: "secret", - EnvConfigName: "myConfig", - EnvViewName: "myView", - }, - expected: "bluecatv2: some credentials information are missing: BLUECATV2_USERNAME", - }, - { - desc: "missing password", - envVars: map[string]string{ - EnvServerURL: "https://example.com/", - EnvUsername: "userA", - EnvPassword: "", - EnvConfigName: "myConfig", - EnvViewName: "myView", - }, - expected: "bluecatv2: some credentials information are missing: BLUECATV2_PASSWORD", - }, - { - desc: "missing configuration name", - envVars: map[string]string{ - EnvServerURL: "https://example.com/", - EnvUsername: "userA", - EnvPassword: "secret", - EnvConfigName: "", - EnvViewName: "myView", - }, - expected: "bluecatv2: some credentials information are missing: BLUECATV2_CONFIG_NAME", - }, - { - desc: "missing view name", - envVars: map[string]string{ - EnvServerURL: "https://example.com/", - EnvUsername: "userA", - EnvPassword: "secret", - EnvConfigName: "myConfig", - EnvViewName: "", - }, - expected: "bluecatv2: some credentials information are missing: BLUECATV2_VIEW_NAME", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "bluecatv2: some credentials information are missing: BLUECATV2_SERVER_URL,BLUECATV2_USERNAME,BLUECATV2_PASSWORD,BLUECATV2_CONFIG_NAME,BLUECATV2_VIEW_NAME", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - serverURL string - username string - password string - configName string - viewName string - expected string - }{ - { - desc: "success", - serverURL: "https://example.com/", - username: "userA", - password: "secret", - configName: "myConfig", - viewName: "myView", - }, - { - desc: "missing server URL", - username: "userA", - password: "secret", - configName: "myConfig", - viewName: "myView", - expected: "bluecatv2: missing server URL", - }, - { - desc: "missing username", - serverURL: "https://example.com/", - password: "secret", - configName: "myConfig", - viewName: "myView", - expected: "bluecatv2: credentials missing", - }, - { - desc: "missing password", - serverURL: "https://example.com/", - username: "userA", - configName: "myConfig", - viewName: "myView", - expected: "bluecatv2: credentials missing", - }, - { - desc: "missing configuration name", - serverURL: "https://example.com/", - username: "userA", - password: "secret", - viewName: "myView", - expected: "bluecatv2: missing configuration name", - }, - { - desc: "missing view name", - serverURL: "https://example.com/", - username: "userA", - password: "secret", - configName: "myConfig", - expected: "bluecatv2: missing view name", - }, - { - desc: "missing credentials", - expected: "bluecatv2: missing server URL", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.ServerURL = test.serverURL - config.Username = test.username - config.Password = test.password - config.ConfigName = test.configName - config.ViewName = test.viewName - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - - config.ServerURL = server.URL - config.Username = "userA" - config.Password = "secret" - config.ConfigName = "myConfiguration" - config.ViewName = "myView" - - config.HTTPClient = server.Client() - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - return p, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("POST /api/v2/sessions", - servermock.ResponseFromInternal("postSession.json"), - servermock.CheckRequestJSONBodyFromInternal("postSession-request.json"), - ). - Route("GET /api/v2/configurations", - servermock.ResponseFromInternal("configurations.json"), - servermock.CheckQueryParameter().Strict(). - With("filter", "name:eq('myConfiguration')"), - ). - Route("GET /api/v2/configurations/12345/views", - servermock.ResponseFromInternal("views.json"), - servermock.CheckQueryParameter().Strict(). - With("filter", "name:eq('myView')"), - ). - Route("GET /api/v2/zones", - http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - filter := req.URL.Query().Get("filter") - - if strings.Contains(filter, internal.Eq("absoluteName", "example.com").String()) { - servermock.ResponseFromInternal("zones.json").ServeHTTP(rw, req) - - return - } - - servermock.ResponseFromInternal("error.json"). - WithStatusCode(http.StatusNotFound).ServeHTTP(rw, req) - }), - ). - Route("POST /api/v2/zones/12345/resourceRecords", - servermock.ResponseFromInternal("postZoneResourceRecord.json"), - servermock.CheckRequestJSONBodyFromInternal("postZoneResourceRecord-request.json"), - ). - Route("POST /api/v2/zones/12345/deployments", - servermock.ResponseFromInternal("postZoneDeployment.json"). - WithStatusCode(http.StatusCreated), - servermock.CheckRequestJSONBodyFromInternal("postZoneDeployment-request.json"), - ). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_Present_skipDeploy(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(map[string]string{ - EnvSkipDeploy: "true", - }) - - provider := mockBuilder(). - Route("POST /api/v2/sessions", - servermock.ResponseFromInternal("postSession.json"), - servermock.CheckRequestJSONBodyFromInternal("postSession-request.json"), - ). - Route("GET /api/v2/configurations", - servermock.ResponseFromInternal("configurations.json"), - servermock.CheckQueryParameter().Strict(). - With("filter", "name:eq('myConfiguration')"), - ). - Route("GET /api/v2/configurations/12345/views", - servermock.ResponseFromInternal("views.json"), - servermock.CheckQueryParameter().Strict(). - With("filter", "name:eq('myView')"), - ). - Route("GET /api/v2/zones", - http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - filter := req.URL.Query().Get("filter") - - if strings.Contains(filter, internal.Eq("absoluteName", "example.com").String()) { - servermock.ResponseFromInternal("zones.json").ServeHTTP(rw, req) - - return - } - - servermock.ResponseFromInternal("error.json"). - WithStatusCode(http.StatusNotFound).ServeHTTP(rw, req) - }), - ). - Route("POST /api/v2/zones/12345/resourceRecords", - servermock.ResponseFromInternal("postZoneResourceRecord.json"), - servermock.CheckRequestJSONBodyFromInternal("postZoneResourceRecord-request.json"), - ). - Route("POST /api/v2/zones/456789/deployments", - servermock.Noop(). - WithStatusCode(http.StatusUnauthorized), - ). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("POST /api/v2/sessions", - servermock.ResponseFromInternal("postSession.json"), - servermock.CheckRequestJSONBodyFromInternal("postSession-request.json"), - ). - Route("DELETE /api/v2/resourceRecords/12345", - servermock.ResponseFromInternal("deleteResourceRecord.json"), - ). - Route("POST /api/v2/zones/456789/deployments", - servermock.ResponseFromInternal("postZoneDeployment.json"). - WithStatusCode(http.StatusCreated), - servermock.CheckRequestJSONBodyFromInternal("postZoneDeployment-request.json"), - ). - Build(t) - - provider.zoneIDs["abc"] = 456789 - provider.recordIDs["abc"] = 12345 - - err := provider.CleanUp("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp_skipDeploy(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(map[string]string{ - EnvSkipDeploy: "true", - }) - - provider := mockBuilder(). - Route("POST /api/v2/sessions", - servermock.ResponseFromInternal("postSession.json"), - servermock.CheckRequestJSONBodyFromInternal("postSession-request.json"), - ). - Route("DELETE /api/v2/resourceRecords/12345", - servermock.ResponseFromInternal("deleteResourceRecord.json"), - ). - Route("POST /api/v2/zones/456789/deployments", - servermock.Noop(). - WithStatusCode(http.StatusUnauthorized), - ). - Build(t) - - provider.zoneIDs["abc"] = 456789 - provider.recordIDs["abc"] = 12345 - - err := provider.CleanUp("example.com", "abc", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/bluecatv2/internal/client.go b/providers/dns/bluecatv2/internal/client.go deleted file mode 100644 index d3c801154..000000000 --- a/providers/dns/bluecatv2/internal/client.go +++ /dev/null @@ -1,221 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" - querystring "github.com/google/go-querystring/query" -) - -// Client the Bluecat v2 API client. -type Client struct { - username string - password string - - baseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(serverURL, username, password string) (*Client, error) { - if serverURL == "" { - return nil, errors.New("server URL missing") - } - - if username == "" || password == "" { - return nil, errors.New("credentials missing") - } - - baseURL, err := url.Parse(serverURL) - if err != nil { - return nil, err - } - - return &Client{ - username: username, - password: password, - baseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -// RetrieveZones retrieves all zones. -func (c *Client) RetrieveZones(ctx context.Context, opts *CollectionOptions) ([]ZoneResource, error) { - endpoint := c.baseURL.JoinPath("api", "v2", "zones") - - collection, err := retrieveCollection[ZoneResource](ctx, c, endpoint, opts) - if err != nil { - return nil, err - } - - return collection.Data, nil -} - -// RetrieveZoneDeployments retrieves all deployments for a zone. -func (c *Client) RetrieveZoneDeployments(ctx context.Context, zoneID int64, opts *CollectionOptions) ([]QuickDeployment, error) { - endpoint := c.baseURL.JoinPath("api", "v2", "zones", strconv.FormatInt(zoneID, 10), "deployments") - - collection, err := retrieveCollection[QuickDeployment](ctx, c, endpoint, opts) - if err != nil { - return nil, err - } - - return collection.Data, nil -} - -// CreateZoneDeployment creates a new deployment for a zone. -func (c *Client) CreateZoneDeployment(ctx context.Context, zoneID int64) (*QuickDeployment, error) { - endpoint := c.baseURL.JoinPath("api", "v2", "zones", strconv.FormatInt(zoneID, 10), "deployments") - - payload := CommonResource{ - Type: "QuickDeployment", - } - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, payload) - if err != nil { - return nil, err - } - - result := new(QuickDeployment) - - err = c.doAuthenticated(ctx, req, result) - if err != nil { - return nil, err - } - - return result, nil -} - -// CreateZoneResourceRecord creates a new TXT record in a zone. -func (c *Client) CreateZoneResourceRecord(ctx context.Context, zoneID int64, record RecordTXT) (*RecordTXT, error) { - endpoint := c.baseURL.JoinPath("api", "v2", "zones", strconv.FormatInt(zoneID, 10), "resourceRecords") - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) - if err != nil { - return nil, err - } - - result := new(RecordTXT) - - err = c.doAuthenticated(ctx, req, result) - if err != nil { - return nil, err - } - - return result, nil -} - -// DeleteResourceRecord deletes a resource record. -func (c *Client) DeleteResourceRecord(ctx context.Context, recordID int64) error { - endpoint := c.baseURL.JoinPath("api", "v2", "resourceRecords", strconv.FormatInt(recordID, 10)) - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return err - } - - return c.doAuthenticated(ctx, req, nil) -} - -func (c *Client) do(req *http.Request, result any) error { - useragent.SetHeader(req.Header) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func retrieveCollection[T any](ctx context.Context, client *Client, endpoint *url.URL, opts *CollectionOptions) (*Collection[T], error) { - if opts != nil { - values, err := querystring.Values(opts) - if err != nil { - return nil, err - } - - endpoint.RawQuery = values.Encode() - } - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - result := &Collection[T]{} - - err = client.doAuthenticated(ctx, req, result) - if err != nil { - return nil, err - } - - return result, nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIError - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return &errAPI -} diff --git a/providers/dns/bluecatv2/internal/client_test.go b/providers/dns/bluecatv2/internal/client_test.go deleted file mode 100644 index 2559af66e..000000000 --- a/providers/dns/bluecatv2/internal/client_test.go +++ /dev/null @@ -1,208 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - "time" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilderAuthenticated() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient(server.URL, "userA", "secret") - if err != nil { - return nil, err - } - - client.baseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(), - servermock.CheckHeader(). - WithAuthorization("Basic secretToken"), - ) -} - -func TestClient_RetrieveZones(t *testing.T) { - client := mockBuilderAuthenticated(). - Route("GET /api/v2/zones", - servermock.ResponseFromFixture("zones.json"), - servermock.CheckQueryParameter().Strict(). - With( - "filter", - "absoluteName:eq('example.com') and configuration.name:eq('myConfiguration') and view.name:eq('myView')", - ), - ). - Build(t) - - opts := &CollectionOptions{ - Filter: And( - Eq("absoluteName", "example.com"), - Eq("configuration.name", "myConfiguration"), - Eq("view.name", "myView"), - ).String(), - } - - result, err := client.RetrieveZones(mockToken(t.Context()), opts) - require.NoError(t, err) - - expected := []ZoneResource{ - { - CommonResource: CommonResource{ID: 12345, Type: "ENUMZone", Name: "5678"}, - AbsoluteName: "string", - }, - { - CommonResource: CommonResource{ID: 12345, Type: "ExternalHostsZone", Name: "name"}, - }, - { - CommonResource: CommonResource{ID: 12345, Type: "InternalRootZone", Name: "name"}, - }, - { - CommonResource: CommonResource{ID: 12345, Type: "ResponsePolicyZone", Name: "name"}, - }, - { - CommonResource: CommonResource{ID: 12345, Type: "Zone", Name: "example.com"}, - AbsoluteName: "example.com", - }, - } - - assert.Equal(t, expected, result) -} - -func TestClient_RetrieveZones_error(t *testing.T) { - client := mockBuilderAuthenticated(). - Route("GET /api/v2/zones", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized), - ). - Build(t) - - opts := &CollectionOptions{ - Filter: And( - Eq("absoluteName", "example.com"), - Eq("configuration.name", "myConfiguration"), - Eq("view.name", "myView"), - ).String(), - } - - _, err := client.RetrieveZones(mockToken(t.Context()), opts) - require.EqualError(t, err, "401: Unauthorized: InvalidAuthorizationToken: The provided authorization token is invalid") -} - -func TestClient_RetrieveZoneDeployments(t *testing.T) { - client := mockBuilderAuthenticated(). - Route("GET /api/v2/zones/456789/deployments", - servermock.ResponseFromFixture("getZoneDeployments.json"), - servermock.CheckQueryParameter().Strict(). - With("filter", "id:eq('12345')"), - ). - Build(t) - - opts := &CollectionOptions{ - Filter: Eq("id", "12345").String(), - } - - result, err := client.RetrieveZoneDeployments(mockToken(t.Context()), 456789, opts) - require.NoError(t, err) - - expected := []QuickDeployment{ - { - CommonResource: CommonResource{ID: 12345, Type: "QuickDeployment", Name: ""}, - State: "PENDING", - Status: "CANCEL", - Message: "string", - PercentComplete: 50, - CreationDateTime: time.Date(2022, time.November, 23, 2, 53, 0, 0, time.UTC), - StartDateTime: time.Date(2022, time.November, 23, 2, 53, 3, 0, time.UTC), - CompletionDateTime: time.Date(2022, time.November, 23, 2, 54, 5, 0, time.UTC), - Method: "SCHEDULED", - }, - } - - assert.Equal(t, expected, result) -} - -func TestClient_CreateZoneDeployment(t *testing.T) { - client := mockBuilderAuthenticated(). - Route("POST /api/v2/zones/12345/deployments", - servermock.ResponseFromFixture("postZoneDeployment.json"). - WithStatusCode(http.StatusCreated), - servermock.CheckRequestJSONBodyFromFixture("postZoneDeployment-request.json"), - ). - Build(t) - - quickDeployment, err := client.CreateZoneDeployment(mockToken(t.Context()), 12345) - require.NoError(t, err) - - expected := &QuickDeployment{ - CommonResource: CommonResource{ID: 12345, Type: "QuickDeployment"}, - State: "PENDING", - Status: "CANCEL", - Message: "string", - PercentComplete: 50, - CreationDateTime: time.Date(2022, time.November, 23, 2, 53, 0, 0, time.UTC), - StartDateTime: time.Date(2022, time.November, 23, 2, 53, 3, 0, time.UTC), - CompletionDateTime: time.Date(2022, time.November, 23, 2, 54, 5, 0, time.UTC), - Method: "SCHEDULED", - } - - assert.Equal(t, expected, quickDeployment) -} - -func TestClient_CreateZoneResourceRecord(t *testing.T) { - client := mockBuilderAuthenticated(). - Route("POST /api/v2/zones/12345/resourceRecords", - servermock.ResponseFromFixture("postZoneResourceRecord.json"), - servermock.CheckRequestJSONBodyFromFixture("postZoneResourceRecord-request.json"), - ). - Build(t) - - record := RecordTXT{ - CommonResource: CommonResource{ - Type: "TXTRecord", - Name: "_acme-challenge", - }, - Text: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: 120, - RecordType: "TXT", - } - - result, err := client.CreateZoneResourceRecord(mockToken(t.Context()), 12345, record) - require.NoError(t, err) - - expected := &RecordTXT{ - CommonResource: CommonResource{ - ID: 12345, - Type: "ResourceRecord", - Name: "name", - }, - TTL: 3600, - AbsoluteName: "host1.example.com", - Comment: "Sample comment.", - Dynamic: true, - RecordType: "CNAME", - Text: "", - } - - assert.Equal(t, expected, result) -} - -func TestClient_DeleteResourceRecord(t *testing.T) { - client := mockBuilderAuthenticated(). - Route("DELETE /api/v2/resourceRecords/12345", - servermock.ResponseFromFixture("deleteResourceRecord.json"), - ). - Build(t) - - err := client.DeleteResourceRecord(mockToken(t.Context()), 12345) - require.NoError(t, err) -} diff --git a/providers/dns/bluecatv2/internal/fixtures/deleteResourceRecord.json b/providers/dns/bluecatv2/internal/fixtures/deleteResourceRecord.json deleted file mode 100644 index 38ae2db6e..000000000 --- a/providers/dns/bluecatv2/internal/fixtures/deleteResourceRecord.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "id": 12345, - "type": "WorkflowRequest", - "state": "APPROVED", - "operation": "ADD_ALIAS_RECORD", - "creator": { - "id": 103307, - "type": "User", - "name": "admin", - "userDefinedFields": { - "udf1": "value1", - "udf2": "value2" - }, - "authenticator": { - "id": 12345, - "type": "Authenticator", - "name": "LDAP authenticator" - }, - "email": "user@example.com", - "phoneNumber": "555-1234", - "securityPrivilege": "NO_ACCESS", - "historyPrivilege": "HIDE", - "accessType": "GUI", - "passwordResetRequired": true, - "accountLocked": true, - "x509Required": true, - "administrativeAccessRights": [ - { - "resourceType": "Event", - "accessLevel": "HIDE" - } - ] - }, - "resourceId": 0, - "resourceType": "ACL", - "fieldUpdates": [ - { - "name": "string", - "value": {}, - "previousValue": {} - } - ], - "dependentRequest": "string", - "modifier": { - "id": 103307, - "type": "User", - "name": "admin", - "userDefinedFields": { - "udf1": "value1", - "udf2": "value2" - }, - "authenticator": { - "id": 12345, - "type": "Authenticator", - "name": "LDAP authenticator" - }, - "email": "user@example.com", - "phoneNumber": "555-1234", - "securityPrivilege": "NO_ACCESS", - "historyPrivilege": "HIDE", - "accessType": "GUI", - "passwordResetRequired": true, - "accountLocked": true, - "x509Required": true, - "administrativeAccessRights": [ - { - "resourceType": "Event", - "accessLevel": "HIDE" - } - ] - }, - "creationDateTime": "2022-10-17T19:11:45Z", - "modificationDateTime": "2022-10-18T19:11:45Z", - "comment": "Sample comment." -} diff --git a/providers/dns/bluecatv2/internal/fixtures/error.json b/providers/dns/bluecatv2/internal/fixtures/error.json deleted file mode 100644 index d3d2b8b5f..000000000 --- a/providers/dns/bluecatv2/internal/fixtures/error.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "status": 401, - "reason": "Unauthorized", - "code": "InvalidAuthorizationToken", - "message": "The provided authorization token is invalid" -} diff --git a/providers/dns/bluecatv2/internal/fixtures/getZoneDeployments.json b/providers/dns/bluecatv2/internal/fixtures/getZoneDeployments.json deleted file mode 100644 index b1a4938ad..000000000 --- a/providers/dns/bluecatv2/internal/fixtures/getZoneDeployments.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "count": 0, - "totalCount": 0, - "data": [ - { - "id": 12345, - "type": "QuickDeployment", - "state": "PENDING", - "status": "CANCEL", - "message": "string", - "percentComplete": 50, - "creationDateTime": "2022-11-23T02:53:00Z", - "startDateTime": "2022-11-23T02:53:03Z", - "completionDateTime": "2022-11-23T02:54:05Z", - "user": { - "id": 103307, - "type": "User", - "name": "admin", - "userDefinedFields": { - "udf1": "value1", - "udf2": "value2" - }, - "authenticator": { - "id": 12345, - "type": "Authenticator", - "name": "LDAP authenticator" - }, - "email": "user@example.com", - "phoneNumber": "555-1234", - "securityPrivilege": "NO_ACCESS", - "historyPrivilege": "HIDE", - "accessType": "GUI", - "passwordResetRequired": true, - "accountLocked": true, - "x509Required": true, - "administrativeAccessRights": [ - { - "resourceType": "Event", - "accessLevel": "HIDE" - } - ] - }, - "method": "SCHEDULED" - } - ] -} diff --git a/providers/dns/bluecatv2/internal/fixtures/postSession-request.json b/providers/dns/bluecatv2/internal/fixtures/postSession-request.json deleted file mode 100644 index e62048eb9..000000000 --- a/providers/dns/bluecatv2/internal/fixtures/postSession-request.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "username": "userA", - "password": "secret" -} diff --git a/providers/dns/bluecatv2/internal/fixtures/postSession.json b/providers/dns/bluecatv2/internal/fixtures/postSession.json deleted file mode 100644 index 4599ad0ad..000000000 --- a/providers/dns/bluecatv2/internal/fixtures/postSession.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "id": 12345, - "type": "UserSession", - "apiToken": "VZoO2Z0BjBaJyvuhE4vNJRWqI9upwDHk70UNi0Ez", - "apiTokenExpirationDateTime": "2022-09-15T17:52:07Z", - "basicAuthenticationCredentials": "YXBpOlQ0OExOT3VIRGhDcnVBNEo1bGFES3JuS3hTZC9QK3pjczZXTzBJMDY=", - "remoteAddress": "192.168.1.1", - "readOnly": true, - "loginDateTime": "2022-09-14T17:45:03Z", - "logoutDateTime": "2022-09-14T19:45:03Z", - "state": "LOGGED_IN", - "response": "Authentication Error: Ensure that your username and password are correct.", - "user": { - "id": 103307, - "type": "User", - "name": "admin", - "userDefinedFields": { - "udf1": "value1", - "udf2": "value2" - }, - "authenticator": { - "id": 12345, - "type": "Authenticator", - "name": "LDAP authenticator" - }, - "email": "user@example.com", - "phoneNumber": "555-1234", - "securityPrivilege": "NO_ACCESS", - "historyPrivilege": "HIDE", - "accessType": "GUI", - "passwordResetRequired": true, - "accountLocked": true, - "x509Required": true, - "administrativeAccessRights": [ - { - "resourceType": "Event", - "accessLevel": "HIDE" - } - ] - }, - "authenticator": { - "id": 12345, - "type": "Authenticator", - "name": "LDAP authenticator", - "userDefinedFields": { - "udf1": "value1", - "udf2": "value2" - } - } -} diff --git a/providers/dns/bluecatv2/internal/fixtures/postZoneDeployment-request.json b/providers/dns/bluecatv2/internal/fixtures/postZoneDeployment-request.json deleted file mode 100644 index 099573a84..000000000 --- a/providers/dns/bluecatv2/internal/fixtures/postZoneDeployment-request.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "QuickDeployment" -} diff --git a/providers/dns/bluecatv2/internal/fixtures/postZoneDeployment.json b/providers/dns/bluecatv2/internal/fixtures/postZoneDeployment.json deleted file mode 100644 index fd26781fb..000000000 --- a/providers/dns/bluecatv2/internal/fixtures/postZoneDeployment.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "id": 12345, - "type": "QuickDeployment", - "state": "PENDING", - "status": "CANCEL", - "message": "string", - "percentComplete": 50, - "creationDateTime": "2022-11-23T02:53:00Z", - "startDateTime": "2022-11-23T02:53:03Z", - "completionDateTime": "2022-11-23T02:54:05Z", - "user": { - "id": 103307, - "type": "User", - "name": "admin", - "userDefinedFields": { - "udf1": "value1", - "udf2": "value2" - }, - "authenticator": { - "id": 12345, - "type": "Authenticator", - "name": "LDAP authenticator" - }, - "email": "user@example.com", - "phoneNumber": "555-1234", - "securityPrivilege": "NO_ACCESS", - "historyPrivilege": "HIDE", - "accessType": "GUI", - "passwordResetRequired": true, - "accountLocked": true, - "x509Required": true, - "administrativeAccessRights": [ - { - "resourceType": "Event", - "accessLevel": "HIDE" - } - ] - }, - "method": "SCHEDULED" -} diff --git a/providers/dns/bluecatv2/internal/fixtures/postZoneResourceRecord-request.json b/providers/dns/bluecatv2/internal/fixtures/postZoneResourceRecord-request.json deleted file mode 100644 index 2de733c71..000000000 --- a/providers/dns/bluecatv2/internal/fixtures/postZoneResourceRecord-request.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "TXTRecord", - "name": "_acme-challenge", - "ttl": 120, - "recordType": "TXT", - "text": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" -} diff --git a/providers/dns/bluecatv2/internal/fixtures/postZoneResourceRecord.json b/providers/dns/bluecatv2/internal/fixtures/postZoneResourceRecord.json deleted file mode 100644 index 78d028ee3..000000000 --- a/providers/dns/bluecatv2/internal/fixtures/postZoneResourceRecord.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "id": 12345, - "type": "ResourceRecord", - "name": "name", - "userDefinedFields": { - "udf1": "value1", - "udf2": "value2" - }, - "configuration": { - "id": 12345, - "type": "Configuration", - "name": "name" - }, - "ttl": 3600, - "absoluteName": "host1.example.com", - "comment": "Sample comment.", - "dynamic": true, - "recordType": "CNAME", - "linkedRecord": { - "id": 12345, - "type": "ResourceRecord", - "name": "name", - "absoluteName": "host1.example.com" - } -} diff --git a/providers/dns/bluecatv2/internal/fixtures/zones.json b/providers/dns/bluecatv2/internal/fixtures/zones.json deleted file mode 100644 index b9f2dfa8f..000000000 --- a/providers/dns/bluecatv2/internal/fixtures/zones.json +++ /dev/null @@ -1,185 +0,0 @@ -{ - "count": 0, - "totalCount": 0, - "data": [ - { - "id": 12345, - "type": "ENUMZone", - "name": "5678", - "userDefinedFields": { - "udf1": "value1", - "udf2": "value2" - }, - "configuration": { - "id": 12345, - "type": "Configuration", - "name": "name" - }, - "view": { - "id": 12345, - "type": "View", - "name": "default", - "userDefinedFields": { - "udf1": "value1", - "udf2": "value2" - }, - "configuration": { - "id": 12345, - "type": "Configuration", - "name": "name" - }, - "deviceRegistrationEnabled": true, - "deviceRegistrationPortalAddress": "10.10.10.10" - }, - "deploymentEnabled": true, - "absoluteName": "string" - }, - { - "id": 12345, - "type": "ExternalHostsZone", - "name": "name", - "userDefinedFields": { - "udf1": "value1", - "udf2": "value2" - }, - "configuration": { - "id": 12345, - "type": "Configuration", - "name": "name" - }, - "view": { - "id": 12345, - "type": "View", - "name": "default", - "userDefinedFields": { - "udf1": "value1", - "udf2": "value2" - }, - "configuration": { - "id": 12345, - "type": "Configuration", - "name": "name" - }, - "deviceRegistrationEnabled": true, - "deviceRegistrationPortalAddress": "10.10.10.10" - } - }, - { - "id": 12345, - "type": "InternalRootZone", - "name": "name", - "userDefinedFields": { - "udf1": "value1", - "udf2": "value2" - }, - "configuration": { - "id": 12345, - "type": "Configuration", - "name": "name" - }, - "view": { - "id": 12345, - "type": "View", - "name": "default", - "userDefinedFields": { - "udf1": "value1", - "udf2": "value2" - }, - "configuration": { - "id": 12345, - "type": "Configuration", - "name": "name" - }, - "deviceRegistrationEnabled": true, - "deviceRegistrationPortalAddress": "10.10.10.10" - }, - "deploymentEnabled": true - }, - { - "id": 12345, - "type": "ResponsePolicyZone", - "name": "name", - "userDefinedFields": { - "udf1": "value1", - "udf2": "value2" - }, - "configuration": { - "id": 12345, - "type": "Configuration", - "name": "name" - }, - "view": { - "id": 12345, - "type": "View", - "name": "default", - "userDefinedFields": { - "udf1": "value1", - "udf2": "value2" - }, - "configuration": { - "id": 12345, - "type": "Configuration", - "name": "name" - }, - "deviceRegistrationEnabled": true, - "deviceRegistrationPortalAddress": "10.10.10.10" - }, - "responsePolicyZoneType": "LOCAL", - "responsePolicy": { - "id": 12345, - "type": "ResponsePolicy", - "name": "Block Response Policy" - }, - "overridePolicyType": "ALLOWLIST", - "overrideRefreshTime": "string", - "redirectTarget": "string", - "feedCategories": [ - "string" - ] - }, - { - "id": 12345, - "type": "Zone", - "name": "example.com", - "userDefinedFields": { - "udf1": "value1", - "udf2": "value2" - }, - "configuration": { - "id": 12345, - "type": "Configuration", - "name": "name" - }, - "view": { - "id": 12345, - "type": "View", - "name": "default", - "userDefinedFields": { - "udf1": "value1", - "udf2": "value2" - }, - "configuration": { - "id": 12345, - "type": "Configuration", - "name": "name" - }, - "deviceRegistrationEnabled": true, - "deviceRegistrationPortalAddress": "10.10.10.10" - }, - "deploymentEnabled": true, - "dynamicUpdateEnabled": true, - "template": { - "id": 12345, - "type": "ZoneTemplate", - "name": "name" - }, - "signed": true, - "signingPolicy": { - "id": 12345, - "type": "DNSSECSigningPolicy", - "name": "name" - }, - "absoluteName": "example.com" - } - ] -} diff --git a/providers/dns/bluecatv2/internal/identity.go b/providers/dns/bluecatv2/internal/identity.go deleted file mode 100644 index af9355ab2..000000000 --- a/providers/dns/bluecatv2/internal/identity.go +++ /dev/null @@ -1,60 +0,0 @@ -package internal - -import ( - "context" - "fmt" - "net/http" -) - -type token string - -const tokenKey token = "token" - -const authorizationHeader = "Authorization" - -// CreateSession creates a new session. -func (c *Client) CreateSession(ctx context.Context, info LoginInfo) (*Session, error) { - endpoint := c.baseURL.JoinPath("api", "v2", "sessions") - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, info) - if err != nil { - return nil, err - } - - result := new(Session) - - err = c.do(req, result) - if err != nil { - return nil, err - } - - return result, nil -} - -// CreateAuthenticatedContext creates a new authenticated context. -func (c *Client) CreateAuthenticatedContext(ctx context.Context) (context.Context, error) { - tok, err := c.CreateSession(ctx, LoginInfo{Username: c.username, Password: c.password}) - if err != nil { - return nil, fmt.Errorf("create session: %w", err) - } - - return context.WithValue(ctx, tokenKey, tok.BasicAuthenticationCredentials), nil -} - -func (c *Client) doAuthenticated(ctx context.Context, req *http.Request, result any) error { - tok := getToken(ctx) - if tok != "" { - req.Header.Set(authorizationHeader, "Basic "+tok) - } - - return c.do(req, result) -} - -func getToken(ctx context.Context) string { - tok, ok := ctx.Value(tokenKey).(string) - if !ok { - return "" - } - - return tok -} diff --git a/providers/dns/bluecatv2/internal/identity_test.go b/providers/dns/bluecatv2/internal/identity_test.go deleted file mode 100644 index 3a1c4d2a2..000000000 --- a/providers/dns/bluecatv2/internal/identity_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package internal - -import ( - "context" - "net/http/httptest" - "net/url" - "testing" - "time" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient(server.URL, "userA", "secret") - if err != nil { - return nil, err - } - - client.baseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(), - ) -} - -func mockToken(ctx context.Context) context.Context { - return context.WithValue(ctx, tokenKey, "secretToken") -} - -func TestClient_CreateSession(t *testing.T) { - client := mockBuilder(). - Route("POST /api/v2/sessions", - servermock.ResponseFromFixture("postSession.json"), - servermock.CheckRequestJSONBodyFromFixture("postSession-request.json"), - ). - Build(t) - - info := LoginInfo{ - Username: "userA", - Password: "secret", - } - - result, err := client.CreateSession(mockToken(t.Context()), info) - require.NoError(t, err) - - expected := &Session{ - ID: 12345, - Type: "UserSession", - APIToken: "VZoO2Z0BjBaJyvuhE4vNJRWqI9upwDHk70UNi0Ez", - APITokenExpirationDateTime: time.Date(2022, time.September, 15, 17, 52, 7, 0, time.UTC), - BasicAuthenticationCredentials: "YXBpOlQ0OExOT3VIRGhDcnVBNEo1bGFES3JuS3hTZC9QK3pjczZXTzBJMDY=", - RemoteAddress: "192.168.1.1", - ReadOnly: true, - LoginDateTime: time.Date(2022, time.September, 14, 17, 45, 3, 0, time.UTC), - LogoutDateTime: time.Date(2022, time.September, 14, 19, 45, 3, 0, time.UTC), - State: "LOGGED_IN", - Response: "Authentication Error: Ensure that your username and password are correct.", - } - - assert.Equal(t, expected, result) -} - -func TestClient_CreateAuthenticatedContext(t *testing.T) { - client := mockBuilder(). - Route("POST /api/v2/sessions", - servermock.ResponseFromFixture("postSession.json"), - servermock.CheckRequestJSONBodyFromFixture("postSession-request.json"), - ). - Build(t) - - ctx, err := client.CreateAuthenticatedContext(t.Context()) - require.NoError(t, err) - - assert.Equal(t, "YXBpOlQ0OExOT3VIRGhDcnVBNEo1bGFES3JuS3hTZC9QK3pjczZXTzBJMDY=", getToken(ctx)) -} diff --git a/providers/dns/bluecatv2/internal/predicates.go b/providers/dns/bluecatv2/internal/predicates.go deleted file mode 100644 index 8ed6f714b..000000000 --- a/providers/dns/bluecatv2/internal/predicates.go +++ /dev/null @@ -1,64 +0,0 @@ -package internal - -import ( - "fmt" - "strings" -) - -type Predicate struct { - field string - operator string - values []string -} - -func (p *Predicate) String() string { - var values []string - for _, v := range p.values { - values = append(values, fmt.Sprintf("'%s'", v)) - } - - return fmt.Sprintf("%s:%s(%s)", p.field, p.operator, strings.Join(values, ", ")) -} - -func Eq(field, value string) *Predicate { - return &Predicate{field: field, operator: "eq", values: []string{value}} -} - -func Contains(field, value string) *Predicate { - return &Predicate{field: field, operator: "contains", values: []string{value}} -} - -func StartsWith(field, value string) *Predicate { - return &Predicate{field: field, operator: "startsWith", values: []string{value}} -} - -func EndsWith(field, value string) *Predicate { - return &Predicate{field: field, operator: "endsWith", values: []string{value}} -} - -func In(field string, values ...string) *Predicate { - return &Predicate{field: field, operator: "in", values: values} -} - -type Combined struct { - predicates []*Predicate - operator string -} - -func (o *Combined) String() string { - var parts []string - - for _, predicate := range o.predicates { - parts = append(parts, predicate.String()) - } - - return strings.Join(parts, " "+o.operator+" ") -} - -func And(predicates ...*Predicate) *Combined { - return &Combined{predicates: predicates, operator: "and"} -} - -func Or(predicates ...*Predicate) *Combined { - return &Combined{predicates: predicates, operator: "or"} -} diff --git a/providers/dns/bluecatv2/internal/predicates_test.go b/providers/dns/bluecatv2/internal/predicates_test.go deleted file mode 100644 index 6913e8729..000000000 --- a/providers/dns/bluecatv2/internal/predicates_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package internal - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestPredicate(t *testing.T) { - testCases := []struct { - desc string - predicate fmt.Stringer - expected string - }{ - { - desc: "Equals", - predicate: Eq("foo", "bar"), - expected: "foo:eq('bar')", - }, - { - desc: "Contains", - predicate: Contains("foo", "bar"), - expected: "foo:contains('bar')", - }, - { - desc: "Starts with", - predicate: StartsWith("foo", "bar"), - expected: "foo:startsWith('bar')", - }, - { - desc: "Ends with", - predicate: EndsWith("foo", "bar"), - expected: "foo:endsWith('bar')", - }, - { - desc: "Match a list of values", - predicate: In("foo", "bar", "bir"), - expected: "foo:in('bar', 'bir')", - }, - { - desc: "Combined: and", - predicate: And(Eq("foo", "bar"), Eq("fii", "bir")), - expected: "foo:eq('bar') and fii:eq('bir')", - }, - { - desc: "Combined: multiple and", - predicate: And( - Eq("foo", "bar"), - Eq("fii", "bir"), - Eq("fuu", "bur"), - ), - expected: "foo:eq('bar') and fii:eq('bir') and fuu:eq('bur')", - }, - { - desc: "Combined: or", - predicate: Or(Eq("foo", "bar"), Eq("foo", "bir")), - expected: "foo:eq('bar') or foo:eq('bir')", - }, - { - desc: "Combined: multiple or", - predicate: Or( - Eq("foo", "bar"), - Eq("foo", "bir"), - Eq("foo", "bur"), - ), - expected: "foo:eq('bar') or foo:eq('bir') or foo:eq('bur')", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - assert.Equal(t, test.expected, test.predicate.String()) - }) - } -} diff --git a/providers/dns/bluecatv2/internal/types.go b/providers/dns/bluecatv2/internal/types.go deleted file mode 100644 index 562fd60b0..000000000 --- a/providers/dns/bluecatv2/internal/types.go +++ /dev/null @@ -1,122 +0,0 @@ -package internal - -import ( - "fmt" - "time" -) - -// Quick deployment states. -// -//nolint:misspell // US vs UK -const ( - QDStatePending = "PENDING" - QDStateQueued = "QUEUED" - QDStateRunning = "RUNNING" - QDStateCancelled = "CANCELLED" - QDStateCancelling = "CANCELLING" - QDStateCompleted = "COMPLETED" - QDStateCompletedWithErrors = "COMPLETED_WITH_ERRORS" - QDStateCompletedWithWarnings = "COMPLETED_WITH_WARNINGS" - QDStateFailed = "FAILED" - QDStateUnknown = "UNKNOWN" -) - -// APIError represents an error. -// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Errors/9.6.0 -type APIError struct { - Status int `json:"status"` - Reason string `json:"reason"` - Code string `json:"code"` - Message string `json:"message"` -} - -func (a *APIError) Error() string { - return fmt.Sprintf("%d: %s: %s: %s", a.Status, a.Reason, a.Code, a.Message) -} - -// CommonResource represents the common resource fields. -// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Resources/9.6.0 -type CommonResource struct { - ID int64 `json:"id,omitempty"` - Type string `json:"type,omitempty"` - Name string `json:"name,omitempty"` -} - -// Collection represents a collection of resources. -// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Collections/9.6.0 -type Collection[T any] struct { - Count int64 `json:"count"` - TotalCount int64 `json:"totalCount"` - Data []T `json:"data"` -} - -type CollectionOptions struct { - // https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Fields/9.6.0 - Fields string `url:"fields,omitempty"` - - // https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Pagination/9.6.0 - Limit int `url:"limit,omitempty"` - Offset int `url:"offset,omitempty"` - - // https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Filter/9.6.0 - Filter string `url:"filter,omitempty"` - - // https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Ordering/9.6.0 - OrderBy string `url:"orderBy,omitempty"` - - // Should return or not the total number of resources matching the query. - Total bool `url:"total,omitempty"` -} - -type RecordTXT struct { - CommonResource - - TTL int `json:"ttl,omitempty"` - AbsoluteName string `json:"absoluteName,omitempty"` - Comment string `json:"comment,omitempty"` - Dynamic bool `json:"dynamic,omitempty"` - RecordType string `json:"recordType,omitempty"` - Text string `json:"text,omitempty"` -} - -type ZoneResource struct { - CommonResource - - AbsoluteName string `json:"absoluteName,omitempty"` -} - -type QuickDeployment struct { - CommonResource - - State string `json:"state,omitempty"` - Status string `json:"status,omitempty"` - Message string `json:"message,omitempty"` - PercentComplete int `json:"percentComplete,omitempty"` - CreationDateTime time.Time `json:"creationDateTime,omitzero"` - StartDateTime time.Time `json:"startDateTime,omitzero"` - CompletionDateTime time.Time `json:"completionDateTime,omitzero"` - Method string `json:"method,omitempty"` -} - -// LoginInfo represents the login information. -// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Creating-an-API-session/9.6.0 -type LoginInfo struct { - Username string `json:"username"` - Password string `json:"password"` -} - -// Session represents the session. -// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Creating-an-API-session/9.6.0 -type Session struct { - ID int `json:"id"` - Type string `json:"type"` - APIToken string `json:"apiToken"` - APITokenExpirationDateTime time.Time `json:"apiTokenExpirationDateTime"` - BasicAuthenticationCredentials string `json:"basicAuthenticationCredentials"` - RemoteAddress string `json:"remoteAddress"` - ReadOnly bool `json:"readOnly"` - LoginDateTime time.Time `json:"loginDateTime"` - LogoutDateTime time.Time `json:"logoutDateTime"` - State string `json:"state"` - Response string `json:"response"` -} diff --git a/providers/dns/bookmyname/bookmyname.go b/providers/dns/bookmyname/bookmyname.go deleted file mode 100644 index 6f42dfd78..000000000 --- a/providers/dns/bookmyname/bookmyname.go +++ /dev/null @@ -1,141 +0,0 @@ -// Package bookmyname implements a DNS provider for solving the DNS-01 challenge using BookMyName. -package bookmyname - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/bookmyname/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "BOOKMYNAME_" - - EnvUsername = envNamespace + "USERNAME" - EnvPassword = envNamespace + "PASSWORD" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - Username string - Password string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for BookMyName. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvUsername, EnvPassword) - if err != nil { - return nil, fmt.Errorf("bookmyname: %w", err) - } - - config := NewDefaultConfig() - config.Username = values[EnvUsername] - config.Password = values[EnvPassword] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for BookMyName. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("bookmyname: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.Username, config.Password) - if err != nil { - return nil, fmt.Errorf("bookmyname: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - record := internal.Record{ - Hostname: dns01.UnFqdn(info.EffectiveFQDN), - Type: "txt", - TTL: d.config.TTL, - Value: info.Value, - } - - err := d.client.AddRecord(context.Background(), record) - if err != nil { - return fmt.Errorf("bookmyname: add record: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - record := internal.Record{ - Hostname: dns01.UnFqdn(info.EffectiveFQDN), - Type: "txt", - TTL: d.config.TTL, - Value: info.Value, - } - - err := d.client.RemoveRecord(context.Background(), record) - if err != nil { - return fmt.Errorf("bookmyname: add record: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/bookmyname/bookmyname.toml b/providers/dns/bookmyname/bookmyname.toml deleted file mode 100644 index 76fcb85e7..000000000 --- a/providers/dns/bookmyname/bookmyname.toml +++ /dev/null @@ -1,24 +0,0 @@ -Name = "BookMyName" -Description = '''''' -URL = "https://www.bookmyname.com/" -Code = "bookmyname" -Since = "v4.23.0" - -Example = ''' -BOOKMYNAME_USERNAME="xxx" \ -BOOKMYNAME_PASSWORD="yyy" \ -lego --dns bookmyname -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - BOOKMYNAME_USERNAME = "Username" - BOOKMYNAME_PASSWORD = "Password" - [Configuration.Additional] - BOOKMYNAME_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - BOOKMYNAME_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - BOOKMYNAME_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - BOOKMYNAME_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://fr.faqs.bookmyname.com/frfaqs/dyndns" diff --git a/providers/dns/bookmyname/internal/client.go b/providers/dns/bookmyname/internal/client.go deleted file mode 100644 index 08d4cccce..000000000 --- a/providers/dns/bookmyname/internal/client.go +++ /dev/null @@ -1,118 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "strings" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - querystring "github.com/google/go-querystring/query" -) - -const defaultBaseURL = "https://www.bookmyname.com/dyndns/" - -// Client the BookMyName API client. -type Client struct { - username string - password string - - baseURL string - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(username, password string) (*Client, error) { - if username == "" || password == "" { - return nil, errors.New("credentials missing") - } - - return &Client{ - username: username, - password: password, - baseURL: defaultBaseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -func (c *Client) AddRecord(ctx context.Context, record Record) error { - endpoint, err := c.createEndpoint(record, "add") - if err != nil { - return err - } - - err = c.do(ctx, endpoint) - if err != nil { - return err - } - - return nil -} - -func (c *Client) RemoveRecord(ctx context.Context, record Record) error { - endpoint, err := c.createEndpoint(record, "remove") - if err != nil { - return err - } - - err = c.do(ctx, endpoint) - if err != nil { - return err - } - - return nil -} - -func (c *Client) createEndpoint(record Record, action string) (*url.URL, error) { - endpoint, err := url.Parse(c.baseURL) - if err != nil { - return nil, fmt.Errorf("parse URL: %w", err) - } - - values, err := querystring.Values(record) - if err != nil { - return nil, fmt.Errorf("query parameters: %w", err) - } - - values.Set("do", action) - - endpoint.RawQuery = values.Encode() - - return endpoint, nil -} - -func (c *Client) do(ctx context.Context, endpoint *url.URL) error { - endpoint.User = url.UserPassword(c.username, c.password) - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody) - if err != nil { - return fmt.Errorf("unable to create request: %w", err) - } - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - if resp.StatusCode/100 != 2 { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - if !strings.HasPrefix(string(raw), "good: update done") && !strings.HasPrefix(string(raw), "good: remove done") { - return fmt.Errorf("unexpected response: %s", string(bytes.TrimSpace(raw))) - } - - return nil -} diff --git a/providers/dns/bookmyname/internal/client_test.go b/providers/dns/bookmyname/internal/client_test.go deleted file mode 100644 index 900d62fef..000000000 --- a/providers/dns/bookmyname/internal/client_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package internal - -import ( - "net/http/httptest" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("user", "secret") - if err != nil { - return nil, err - } - - client.HTTPClient = server.Client() - client.baseURL = server.URL - - return client, nil - }, - servermock.CheckHeader(). - WithBasicAuth("user", "secret")) -} - -func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("GET /", - servermock.ResponseFromFixture("add_success.txt"), - servermock.CheckQueryParameter().Strict(). - With("do", "add"). - With("hostname", "_acme-challenge.sub.example.com."). - With("type", "txt"). - With("value", "test"). - With("ttl", "300"), - ). - Build(t) - - record := Record{ - Hostname: "_acme-challenge.sub.example.com.", - Type: "txt", - TTL: 300, - Value: "test", - } - - err := client.AddRecord(t.Context(), record) - require.NoError(t, err) -} - -func TestClient_AddRecord_error(t *testing.T) { - client := mockBuilder(). - Route("GET /", - servermock.ResponseFromFixture("error.txt"), - servermock.CheckQueryParameter(). - With("do", "add")). - Build(t) - - record := Record{ - Hostname: "_acme-challenge.sub.example.com.", - Type: "txt", - TTL: 300, - Value: "test", - } - - err := client.AddRecord(t.Context(), record) - require.Error(t, err) - - require.EqualError(t, err, "unexpected response: notfqdn: Host _acme-challenge.sub.example.com. malformed / vhn") -} - -func TestClient_RemoveRecord(t *testing.T) { - client := mockBuilder(). - Route("GET /", - servermock.ResponseFromFixture("remove_success.txt"), - servermock.CheckQueryParameter().Strict(). - With("do", "remove"). - With("hostname", "_acme-challenge.sub.example.com."). - With("type", "txt"). - With("value", "test"). - With("ttl", "300"), - ). - Build(t) - - record := Record{ - Hostname: "_acme-challenge.sub.example.com.", - Type: "txt", - TTL: 300, - Value: "test", - } - - err := client.RemoveRecord(t.Context(), record) - require.NoError(t, err) -} - -func TestClient_RemoveRecord_error(t *testing.T) { - client := mockBuilder(). - Route("GET /", - servermock.ResponseFromFixture("error.txt"), - servermock.CheckQueryParameter(). - With("do", "remove")). - Build(t) - - record := Record{ - Hostname: "_acme-challenge.sub.example.com.", - Type: "txt", - TTL: 300, - Value: "test", - } - - err := client.RemoveRecord(t.Context(), record) - require.Error(t, err) - - require.EqualError(t, err, "unexpected response: notfqdn: Host _acme-challenge.sub.example.com. malformed / vhn") -} diff --git a/providers/dns/bookmyname/internal/fixtures/add_success.txt b/providers/dns/bookmyname/internal/fixtures/add_success.txt deleted file mode 100644 index 76304fc24..000000000 --- a/providers/dns/bookmyname/internal/fixtures/add_success.txt +++ /dev/null @@ -1 +0,0 @@ -good: update done, cid 123, domain id 456, type txt, ip xxx diff --git a/providers/dns/bookmyname/internal/fixtures/error.txt b/providers/dns/bookmyname/internal/fixtures/error.txt deleted file mode 100644 index 3c62ede60..000000000 --- a/providers/dns/bookmyname/internal/fixtures/error.txt +++ /dev/null @@ -1 +0,0 @@ -notfqdn: Host _acme-challenge.sub.example.com. malformed / vhn diff --git a/providers/dns/bookmyname/internal/fixtures/remove_success.txt b/providers/dns/bookmyname/internal/fixtures/remove_success.txt deleted file mode 100644 index 1e83c6dcc..000000000 --- a/providers/dns/bookmyname/internal/fixtures/remove_success.txt +++ /dev/null @@ -1 +0,0 @@ -good: remove done 1, cid 123, domain id 456, ttl 300, type txt, ip xxx diff --git a/providers/dns/bookmyname/internal/types.go b/providers/dns/bookmyname/internal/types.go deleted file mode 100644 index 96dab064a..000000000 --- a/providers/dns/bookmyname/internal/types.go +++ /dev/null @@ -1,8 +0,0 @@ -package internal - -type Record struct { - Hostname string `url:"hostname"` - Type string `url:"type"` - TTL int `url:"ttl"` - Value string `url:"value"` -} diff --git a/providers/dns/brandit/brandit.go b/providers/dns/brandit/brandit.go index fe3b52239..5d7b23d01 100644 --- a/providers/dns/brandit/brandit.go +++ b/providers/dns/brandit/brandit.go @@ -9,11 +9,9 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/brandit/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -29,8 +27,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string @@ -93,8 +89,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, client: client, @@ -168,7 +162,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { d.recordsMu.Lock() dnsRecord, ok := d.records[token] d.recordsMu.Unlock() - if !ok { return fmt.Errorf("brandit: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) } @@ -187,7 +180,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } var recordID int - for i, r := range records.RR { if r == dnsRecord { recordID = i diff --git a/providers/dns/brandit/brandit.toml b/providers/dns/brandit/brandit.toml index 4c43e27a9..acf61bd7f 100644 --- a/providers/dns/brandit/brandit.toml +++ b/providers/dns/brandit/brandit.toml @@ -1,10 +1,5 @@ -Name = "Brandit (deprecated)" -Description = ''' -Brandit has been acquired by Abion. -Abion has a different API. - -If you are a Brandit/Albion user, you can try the PR https://github.com/go-acme/lego/pull/2112. -''' +Name = "Brandit" +Description = '''''' URL = "https://www.brandit.com/" Code = "brandit" Since = "v4.11.0" @@ -12,7 +7,7 @@ Since = "v4.11.0" Example = ''' BRANDIT_API_KEY=xxxxxxxxxxxxxxxxxxxxx \ BRANDIT_API_USERNAME=yyyyyyyyyyyyyyyyyyyy \ -lego --dns brandit -d '*.example.com' -d example.com run +lego --email myemail@example.com --dns brandit --domains my.example.org run ''' [Configuration] @@ -20,10 +15,10 @@ lego --dns brandit -d '*.example.com' -d example.com run BRANDIT_API_KEY = "The API key" BRANDIT_API_USERNAME = "The API username" [Configuration.Additional] - BRANDIT_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - BRANDIT_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 600)" - BRANDIT_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)" - BRANDIT_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + BRANDIT_POLLING_INTERVAL = "Time between DNS propagation check" + BRANDIT_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + BRANDIT_TTL = "The TTL of the TXT record used for the DNS challenge" + BRANDIT_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://portal.brandit.com/apidocv3" diff --git a/providers/dns/brandit/brandit_test.go b/providers/dns/brandit/brandit_test.go index 40abdd3d0..156e7c3f4 100644 --- a/providers/dns/brandit/brandit_test.go +++ b/providers/dns/brandit/brandit_test.go @@ -48,7 +48,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -121,7 +120,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -135,7 +133,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/brandit/internal/client.go b/providers/dns/brandit/internal/client.go index cda3be5a2..cba8eefc1 100644 --- a/providers/dns/brandit/internal/client.go +++ b/providers/dns/brandit/internal/client.go @@ -62,7 +62,6 @@ func (c *Client) ListRecords(ctx context.Context, account, dnsZone string) (*Lis query.Add("first", strconv.Itoa(result.Response.Last[0]+1)) tmp := &Response[*ListRecordsResponse]{} - err := c.do(ctx, query, tmp) if err != nil { return nil, err @@ -157,7 +156,6 @@ func (c *Client) do(ctx context.Context, query url.Values, result any) error { // Unmarshal the error response, because the API returns a 200 OK even if there is an error. var apiError APIError - err = json.Unmarshal(raw, &apiError) if err != nil { return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) @@ -180,13 +178,17 @@ func (c *Client) do(ctx context.Context, query url.Values, result any) error { } func sign(apiUsername, apiKey string, query url.Values) (url.Values, error) { - timestamp := time.Now().UTC().Format("2006-01-02T15:04:05Z") + location, err := time.LoadLocation("GMT") + if err != nil { + return nil, fmt.Errorf("time location: %w", err) + } + + timestamp := time.Now().In(location).Format("2006-01-02T15:04:05Z") canonicalRequest := fmt.Sprintf("%s%s%s", apiUsername, timestamp, defaultBaseURL) mac := hmac.New(sha256.New, []byte(apiKey)) - - _, err := mac.Write([]byte(canonicalRequest)) + _, err = mac.Write([]byte(canonicalRequest)) if err != nil { return nil, err } diff --git a/providers/dns/brandit/internal/client_test.go b/providers/dns/brandit/internal/client_test.go index cb779ef68..a37e51a29 100644 --- a/providers/dns/brandit/internal/client_test.go +++ b/providers/dns/brandit/internal/client_test.go @@ -1,44 +1,52 @@ package internal import ( + "context" + "io" + "net/http" "net/http/httptest" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("user", "secret") - if err != nil { - return nil, err - } +func setupTest(t *testing.T, filename string) *Client { + t.Helper() - client.HTTPClient = server.Client() - client.baseURL = server.URL + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } - return client, nil - }, - servermock.CheckHeader(). - WithContentTypeFromURLEncoded()) + defer func() { _ = file.Close() }() + + rw.WriteHeader(http.StatusOK) + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + })) + t.Cleanup(server.Close) + + client, err := NewClient("test_user", "apiKey") + require.NoError(t, err) + + client.HTTPClient = server.Client() + client.baseURL = server.URL + + return client } func TestClient_StatusDomain(t *testing.T) { - client := mockBuilder(). - Route("POST /", servermock.ResponseFromFixture("status-domain.json"), - servermock.CheckForm().Strict(). - WithRegexp("signature", "[a-z0-9]+"). - WithRegexp("timestamp", `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z`). - With("command", "statusDomain"). - With("user", "user"). - With("domain", "example.com"), - ). - Build(t) + client := setupTest(t, "status-domain.json") - domain, err := client.StatusDomain(t.Context(), "example.com") + domain, err := client.StatusDomain(context.Background(), "example.com") require.NoError(t, err) expected := &StatusResponse{ @@ -72,28 +80,16 @@ func TestClient_StatusDomain(t *testing.T) { } func TestClient_StatusDomain_error(t *testing.T) { - client := mockBuilder(). - Route("POST /", servermock.ResponseFromFixture("error.json")). - Build(t) + client := setupTest(t, "error.json") - _, err := client.StatusDomain(t.Context(), "example.com") + _, err := client.StatusDomain(context.Background(), "example.com") require.ErrorIs(t, err, APIError{Code: 402, Status: "error", Message: "Invalid user."}) } func TestClient_ListRecords(t *testing.T) { - client := mockBuilder(). - Route("POST /", servermock.ResponseFromFixture("list-records.json"), - servermock.CheckForm().Strict(). - WithRegexp("signature", "[a-z0-9]+"). - WithRegexp("timestamp", `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z`). - With("account", "example"). - With("command", "listDNSRR"). - With("user", "user"). - With("dnszone", "example.com"), - ). - Build(t) + client := setupTest(t, "list-records.json") - resp, err := client.ListRecords(t.Context(), "example", "example.com") + resp, err := client.ListRecords(context.Background(), "example", "example.com") require.NoError(t, err) expected := &ListRecordsResponse{ @@ -110,28 +106,14 @@ func TestClient_ListRecords(t *testing.T) { } func TestClient_ListRecords_error(t *testing.T) { - client := mockBuilder(). - Route("POST /", servermock.ResponseFromFixture("error.json")). - Build(t) + client := setupTest(t, "error.json") - _, err := client.ListRecords(t.Context(), "example", "example.com") + _, err := client.ListRecords(context.Background(), "example", "example.com") require.ErrorIs(t, err, APIError{Code: 402, Status: "error", Message: "Invalid user."}) } func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /", servermock.ResponseFromFixture("add-record.json"), - servermock.CheckForm().Strict(). - WithRegexp("signature", "[a-z0-9]+"). - WithRegexp("timestamp", `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z`). - With("account", "test"). - With("command", "addDNSRR"). - With("key", "2565"). - With("user", "user"). - With("rrdata", "example.com 600 IN TXT txttxttxt"). - With("dnszone", "example.com"), - ). - Build(t) + client := setupTest(t, "add-record.json") testRecord := Record{ ID: 2565, @@ -140,7 +122,7 @@ func TestClient_AddRecord(t *testing.T) { Content: "txttxttxt", TTL: 600, } - resp, err := client.AddRecord(t.Context(), "example.com", "test", "2565", testRecord) + resp, err := client.AddRecord(context.Background(), "example.com", "test", "2565", testRecord) require.NoError(t, err) expected := &AddRecord{ @@ -158,9 +140,7 @@ func TestClient_AddRecord(t *testing.T) { } func TestClient_AddRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /", servermock.ResponseFromFixture("error.json")). - Build(t) + client := setupTest(t, "error.json") testRecord := Record{ ID: 2565, @@ -170,34 +150,20 @@ func TestClient_AddRecord_error(t *testing.T) { TTL: 600, } - _, err := client.AddRecord(t.Context(), "example.com", "test", "2565", testRecord) + _, err := client.AddRecord(context.Background(), "example.com", "test", "2565", testRecord) require.ErrorIs(t, err, APIError{Code: 402, Status: "error", Message: "Invalid user."}) } func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /", servermock.ResponseFromFixture("delete-record.json"), - servermock.CheckForm().Strict(). - WithRegexp("signature", "[a-z0-9]+"). - WithRegexp("timestamp", `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z`). - With("account", "test"). - With("command", "deleteDNSRR"). - With("key", "2374"). - With("user", "user"). - With("rrdata", "example.com 600 IN TXT txttxttxt"). - With("dnszone", "example.com"), - ). - Build(t) + client := setupTest(t, "delete-record.json") - err := client.DeleteRecord(t.Context(), "example.com", "test", "example.com 600 IN TXT txttxttxt", "2374") + err := client.DeleteRecord(context.Background(), "example.com", "test", "example.com 600 IN TXT txttxttxt", "2374") require.NoError(t, err) } func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /", servermock.ResponseFromFixture("error.json")). - Build(t) + client := setupTest(t, "error.json") - err := client.DeleteRecord(t.Context(), "example.com", "test", "example.com 600 IN TXT txttxttxt", "2374") + err := client.DeleteRecord(context.Background(), "example.com", "test", "example.com 600 IN TXT txttxttxt", "2374") require.ErrorIs(t, err, APIError{Code: 402, Status: "error", Message: "Invalid user."}) } diff --git a/providers/dns/bunny/bunny.go b/providers/dns/bunny/bunny.go index 29949608b..276db09cc 100644 --- a/providers/dns/bunny/bunny.go +++ b/providers/dns/bunny/bunny.go @@ -5,20 +5,15 @@ import ( "context" "errors" "fmt" - "net/http" - "slices" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/internal/ptr" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" "github.com/nrdcg/bunny-go" - "golang.org/x/net/publicsuffix" ) +const minTTL = 60 + // Environment variables names. const ( envNamespace = "BUNNY_" @@ -28,21 +23,14 @@ const ( EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const minTTL = 60 - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { - APIKey string - + APIKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int - HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. @@ -50,10 +38,7 @@ func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), } } @@ -91,19 +76,9 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, fmt.Errorf("bunny: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) } - if config.HTTPClient == nil { - config.HTTPClient = &http.Client{Timeout: 30 * time.Second} - } + client := bunny.NewClient(config.APIKey) - config.HTTPClient = clientdebug.Wrap(config.HTTPClient) - - return &DNSProvider{ - config: config, - client: bunny.NewClient(config.APIKey, - bunny.WithUserAgent(useragent.Get()), - bunny.WithHTTPClient(config.HTTPClient), - ), - }, nil + return &DNSProvider{config: config, client: client}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. @@ -116,27 +91,32 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) + authZone, err := getZone(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("bunny: could not find zone for domain %q: %w", domain, err) + } + ctx := context.Background() - zone, err := d.findZone(ctx, dns01.UnFqdn(info.EffectiveFQDN)) + zone, err := d.findZone(ctx, authZone) if err != nil { return fmt.Errorf("bunny: %w", err) } - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, ptr.Deref(zone.Domain)) + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) if err != nil { return fmt.Errorf("bunny: %w", err) } record := &bunny.AddOrUpdateDNSRecordOptions{ - Type: ptr.Pointer(bunny.DNSRecordTypeTXT), - Name: ptr.Pointer(subDomain), - Value: ptr.Pointer(info.Value), - TTL: ptr.Pointer(int32(d.config.TTL)), + Type: pointer(bunny.DNSRecordTypeTXT), + Name: pointer(subDomain), + Value: pointer(info.Value), + TTL: pointer(int32(d.config.TTL)), } - if _, err := d.client.DNSZone.AddDNSRecord(ctx, ptr.Deref(zone.ID), record); err != nil { - return fmt.Errorf("bunny: failed to add TXT record: fqdn=%s, zoneID=%d: %w", info.EffectiveFQDN, ptr.Deref(zone.ID), err) + if _, err := d.client.DNSZone.AddDNSRecord(ctx, deref(zone.ID), record); err != nil { + return fmt.Errorf("bunny: failed to add TXT record: fqdn=%s, zoneID=%d: %w", info.EffectiveFQDN, deref(zone.ID), err) } return nil @@ -146,35 +126,38 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) + authZone, err := getZone(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("bunny: could not find zone for domain %q: %w", domain, err) + } + ctx := context.Background() - zone, err := d.findZone(ctx, dns01.UnFqdn(info.EffectiveFQDN)) + zone, err := d.findZone(ctx, authZone) if err != nil { return fmt.Errorf("bunny: %w", err) } - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, ptr.Deref(zone.Domain)) + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) if err != nil { return fmt.Errorf("bunny: %w", err) } var record *bunny.DNSRecord - for _, r := range zone.Records { - if ptr.Deref(r.Name) == subDomain && ptr.Deref(r.Type) == bunny.DNSRecordTypeTXT { + if deref(r.Name) == subDomain && deref(r.Type) == bunny.DNSRecordTypeTXT { r := r record = &r - break } } if record == nil { - return fmt.Errorf("bunny: could not find TXT record zone=%d, subdomain=%s", ptr.Deref(zone.ID), subDomain) + return fmt.Errorf("bunny: could not find TXT record zone=%d, subdomain=%s", deref(zone.ID), subDomain) } - if err := d.client.DNSZone.DeleteDNSRecord(ctx, ptr.Deref(zone.ID), ptr.Deref(record.ID)); err != nil { - return fmt.Errorf("bunny: failed to delete TXT record: id=%d, name=%s: %w", ptr.Deref(record.ID), ptr.Deref(record.Name), err) + if err := d.client.DNSZone.DeleteDNSRecord(ctx, deref(zone.ID), deref(record.ID)); err != nil { + return fmt.Errorf("bunny: failed to delete TXT record: id=%d, name=%s: %w", deref(record.ID), deref(record.Name), err) } return nil @@ -186,50 +169,39 @@ func (d *DNSProvider) findZone(ctx context.Context, authZone string) (*bunny.DNS return nil, err } - zone := findZone(zones, authZone) + var zone *bunny.DNSZone + for _, item := range zones.Items { + if item != nil && deref(item.Domain) == authZone { + zone = item + break + } + } + if zone == nil { - return nil, fmt.Errorf("could not find DNSZone domain=%s", authZone) + return nil, fmt.Errorf("could not find DNSZone zone=%s", authZone) } return zone, nil } -func findZone(zones *bunny.DNSZones, domain string) *bunny.DNSZone { - domains := possibleDomains(domain) - - var domainLength int - - var zone *bunny.DNSZone - - for _, item := range zones.Items { - if item == nil { - continue - } - - curr := ptr.Deref(item.Domain) - - if slices.Contains(domains, curr) && domainLength < len(curr) { - domainLength = len(curr) - - zone = item - } +func getZone(fqdn string) (string, error) { + authZone, err := dns01.FindZoneByFqdn(fqdn) + if err != nil { + return "", err } - return zone + zone := dns01.UnFqdn(authZone) + + return zone, nil } -func possibleDomains(domain string) []string { - var domains []string +func pointer[T string | int | int32 | int64](v T) *T { return &v } - tld, _ := publicsuffix.PublicSuffix(domain) - for d := range dns01.DomainsSeq(domain) { - if tld == d { - // skip the TLD - break - } - - domains = append(domains, dns01.UnFqdn(d)) +func deref[T string | int | int32 | int64](v *T) T { + if v == nil { + var zero T + return zero } - return domains + return *v } diff --git a/providers/dns/bunny/bunny.toml b/providers/dns/bunny/bunny.toml index 758c4f202..93ccfadbe 100644 --- a/providers/dns/bunny/bunny.toml +++ b/providers/dns/bunny/bunny.toml @@ -6,17 +6,16 @@ Since = "v4.11.0" Example = ''' BUNNY_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ -lego --dns bunny -d '*.example.com' -d example.com run +lego --email you@example.com --dns bunny --domains my.example.org run ''' [Configuration] [Configuration.Credentials] BUNNY_API_KEY = "API key" [Configuration.Additional] - BUNNY_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - BUNNY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - BUNNY_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" - BUNNY_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + BUNNY_POLLING_INTERVAL = "Time between DNS propagation check" + BUNNY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + BUNNY_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://docs.bunny.net/reference/dnszonepublic_index" diff --git a/providers/dns/bunny/bunny_test.go b/providers/dns/bunny/bunny_test.go index ca4e821e0..e5724bcd2 100644 --- a/providers/dns/bunny/bunny_test.go +++ b/providers/dns/bunny/bunny_test.go @@ -4,9 +4,6 @@ import ( "testing" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/providers/dns/internal/ptr" - "github.com/nrdcg/bunny-go" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -40,7 +37,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -108,7 +104,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -122,124 +117,9 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } - -func Test_findZone(t *testing.T) { - testCases := []struct { - desc string - domain string - items []*bunny.DNSZone - expected *bunny.DNSZone - }{ - { - desc: "found subdomain", - domain: "_acme-challenge.foo.bar.example.com", - items: []*bunny.DNSZone{ - {ID: ptr.Pointer[int64](1), Domain: ptr.Pointer("example.com")}, - {ID: ptr.Pointer[int64](2), Domain: ptr.Pointer("example.org")}, - {ID: ptr.Pointer[int64](4), Domain: ptr.Pointer("bar.example.org")}, - {ID: ptr.Pointer[int64](5), Domain: ptr.Pointer("bar.example.com")}, - {ID: ptr.Pointer[int64](6), Domain: ptr.Pointer("foo.example.com")}, - }, - expected: &bunny.DNSZone{ - ID: ptr.Pointer[int64](5), - Domain: ptr.Pointer("bar.example.com"), - }, - }, - { - desc: "found the longest subdomain", - domain: "_acme-challenge.foo.bar.example.com", - items: []*bunny.DNSZone{ - {ID: ptr.Pointer[int64](7), Domain: ptr.Pointer("foo.bar.example.com")}, - {ID: ptr.Pointer[int64](1), Domain: ptr.Pointer("example.com")}, - {ID: ptr.Pointer[int64](2), Domain: ptr.Pointer("example.org")}, - {ID: ptr.Pointer[int64](4), Domain: ptr.Pointer("bar.example.org")}, - {ID: ptr.Pointer[int64](5), Domain: ptr.Pointer("bar.example.com")}, - {ID: ptr.Pointer[int64](6), Domain: ptr.Pointer("foo.example.com")}, - }, - expected: &bunny.DNSZone{ - ID: ptr.Pointer[int64](7), - Domain: ptr.Pointer("foo.bar.example.com"), - }, - }, - { - desc: "found apex", - domain: "_acme-challenge.foo.bar.example.com", - items: []*bunny.DNSZone{ - {ID: ptr.Pointer[int64](1), Domain: ptr.Pointer("example.com")}, - {ID: ptr.Pointer[int64](2), Domain: ptr.Pointer("example.org")}, - {ID: ptr.Pointer[int64](4), Domain: ptr.Pointer("bar.example.org")}, - {ID: ptr.Pointer[int64](6), Domain: ptr.Pointer("foo.example.com")}, - }, - expected: &bunny.DNSZone{ - ID: ptr.Pointer[int64](1), - Domain: ptr.Pointer("example.com"), - }, - }, - { - desc: "not found", - domain: "_acme-challenge.foo.bar.example.com", - items: []*bunny.DNSZone{ - {ID: ptr.Pointer[int64](2), Domain: ptr.Pointer("example.org")}, - {ID: ptr.Pointer[int64](4), Domain: ptr.Pointer("bar.example.org")}, - {ID: ptr.Pointer[int64](6), Domain: ptr.Pointer("foo.example.com")}, - }, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - zones := &bunny.DNSZones{Items: test.items} - - zone := findZone(zones, test.domain) - - assert.Equal(t, test.expected, zone) - }) - } -} - -func Test_possibleDomains(t *testing.T) { - testCases := []struct { - desc string - domain string - expected []string - }{ - { - desc: "apex", - domain: "example.com", - expected: []string{"example.com"}, - }, - { - desc: "CCTLD", - domain: "example.co.uk", - expected: []string{"example.co.uk"}, - }, - { - desc: "long domain", - domain: "_acme-challenge.foo.bar.example.com", - expected: []string{"_acme-challenge.foo.bar.example.com", "foo.bar.example.com", "bar.example.com", "example.com"}, - }, - { - desc: "empty", - domain: "", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - domains := possibleDomains(test.domain) - - assert.Equal(t, test.expected, domains) - }) - } -} diff --git a/providers/dns/checkdomain/checkdomain.go b/providers/dns/checkdomain/checkdomain.go index 4bc926ed9..4f1e7c137 100644 --- a/providers/dns/checkdomain/checkdomain.go +++ b/providers/dns/checkdomain/checkdomain.go @@ -9,11 +9,9 @@ import ( "net/url" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/checkdomain/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -29,8 +27,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Endpoint *url.URL @@ -73,7 +69,6 @@ func NewDNSProvider() (*DNSProvider, error) { if err != nil { return nil, fmt.Errorf("checkdomain: invalid %s: %w", EnvEndpoint, err) } - config.Endpoint = endpoint return NewDNSProviderConfig(config) @@ -88,11 +83,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("checkdomain: missing token") } - client := internal.NewClient( - clientdebug.Wrap( - internal.OAuthStaticAccessToken(config.HTTPClient, config.Token), - ), - ) + client := internal.NewClient(internal.OAuthStaticAccessToken(config.HTTPClient, config.Token)) if config.Endpoint != nil { client.BaseURL = config.Endpoint diff --git a/providers/dns/checkdomain/checkdomain.toml b/providers/dns/checkdomain/checkdomain.toml index 0b93058ba..854fab3d7 100644 --- a/providers/dns/checkdomain/checkdomain.toml +++ b/providers/dns/checkdomain/checkdomain.toml @@ -6,7 +6,7 @@ Since = "v3.3.0" Example = ''' CHECKDOMAIN_TOKEN=yoursecrettoken \ -lego --dns checkdomain -d '*.example.com' -d example.com run +lego --email you@example.com --dns checkdomain --domains my.example.org run ''' [Configuration] @@ -14,10 +14,10 @@ lego --dns checkdomain -d '*.example.com' -d example.com run CHECKDOMAIN_TOKEN = "API token" [Configuration.Additional] CHECKDOMAIN_ENDPOINT = "API endpoint URL, defaults to https://api.checkdomain.de" - CHECKDOMAIN_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - CHECKDOMAIN_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 300)" - CHECKDOMAIN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 7)" - CHECKDOMAIN_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + CHECKDOMAIN_TTL = "The TTL of the TXT record used for the DNS challenge" + CHECKDOMAIN_POLLING_INTERVAL = "Time between DNS propagation check" + CHECKDOMAIN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + CHECKDOMAIN_HTTP_TIMEOUT = "API request timeout, defaults to 30 seconds" [Links] API = "https://developer.checkdomain.de/reference/" diff --git a/providers/dns/checkdomain/checkdomain_test.go b/providers/dns/checkdomain/checkdomain_test.go index b2c940f7a..d9d0b62a6 100644 --- a/providers/dns/checkdomain/checkdomain_test.go +++ b/providers/dns/checkdomain/checkdomain_test.go @@ -46,7 +46,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -109,7 +108,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -123,7 +121,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/checkdomain/internal/client.go b/providers/dns/checkdomain/internal/client.go index 68d090755..74189dee4 100644 --- a/providers/dns/checkdomain/internal/client.go +++ b/providers/dns/checkdomain/internal/client.go @@ -36,11 +36,11 @@ const maxInt = int((^uint(0)) >> 1) // Client the Autodns API client. type Client struct { - BaseURL *url.URL - httpClient *http.Client - domainIDMapping map[string]int domainIDMu sync.Mutex + + BaseURL *url.URL + httpClient *http.Client } // NewClient creates a new Client. @@ -63,7 +63,6 @@ func (c *Client) GetDomainIDByName(ctx context.Context, name string) (int, error c.domainIDMu.Lock() id, ok := c.domainIDMapping[name] c.domainIDMu.Unlock() - if ok { return id, nil } @@ -101,7 +100,6 @@ func (c *Client) listDomains(ctx context.Context) ([]*Domain, error) { totalPages := maxInt var domainList []*Domain - for currentPage <= totalPages { q.Set("page", strconv.Itoa(currentPage)) endpoint.RawQuery = q.Encode() @@ -153,7 +151,6 @@ func (c *Client) CheckNameservers(ctx context.Context, domainID int) error { } var found1, found2 bool - for _, item := range info.Nameservers { switch item.Name { case ns1: @@ -232,7 +229,6 @@ func (c *Client) getDomainInfo(ctx context.Context, domainID int) (*DomainRespon } var res DomainResponse - err = c.do(req, &res) if err != nil { return nil, err @@ -246,7 +242,6 @@ func (c *Client) listRecords(ctx context.Context, domainID int, recordType strin q := endpoint.Query() q.Set("limit", strconv.Itoa(maxLimit)) - if recordType != "" { q.Set("type", recordType) } @@ -255,7 +250,6 @@ func (c *Client) listRecords(ctx context.Context, domainID int, recordType strin totalPages := maxInt var recordList []*Record - for currentPage <= totalPages { q.Set("page", strconv.Itoa(currentPage)) endpoint.RawQuery = q.Encode() diff --git a/providers/dns/checkdomain/internal/client_test.go b/providers/dns/checkdomain/internal/client_test.go index 68e4f1244..3f6a7e7a7 100644 --- a/providers/dns/checkdomain/internal/client_test.go +++ b/providers/dns/checkdomain/internal/client_test.go @@ -1,66 +1,138 @@ package internal import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" "net/http/httptest" "net/url" + "reflect" "testing" "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient(OAuthStaticAccessToken(server.Client(), "secret")) - client.BaseURL, _ = url.Parse(server.URL) +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("Bearer secret")) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient(OAuthStaticAccessToken(server.Client(), "secret")) + client.BaseURL, _ = url.Parse(server.URL) + + return client, mux +} + +func checkAuthorizationHeader(req *http.Request) error { + val := req.Header.Get("Authorization") + if val != "Bearer secret" { + return fmt.Errorf("invalid header value, got: %s want %s", val, "Bearer secret") + } + return nil } func TestClient_GetDomainIDByName(t *testing.T) { - client := mockBuilder(). - Route("GET /v1/domains", - servermock.JSONEncode(DomainListingResponse{ - Embedded: EmbeddedDomainList{Domains: []*Domain{ - {ID: 1, Name: "test.com"}, - {ID: 2, Name: "test.org"}, - }}, - })). - Build(t) + client, mux := setupTest(t) - id, err := client.GetDomainIDByName(t.Context(), "test.com") + mux.HandleFunc("/v1/domains", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + err := checkAuthorizationHeader(req) + if err != nil { + http.Error(rw, err.Error(), http.StatusUnauthorized) + return + } + + domainList := DomainListingResponse{ + Embedded: EmbeddedDomainList{Domains: []*Domain{ + {ID: 1, Name: "test.com"}, + {ID: 2, Name: "test.org"}, + }}, + } + + err = json.NewEncoder(rw).Encode(domainList) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + id, err := client.GetDomainIDByName(context.Background(), "test.com") require.NoError(t, err) assert.Equal(t, 1, id) } func TestClient_CheckNameservers(t *testing.T) { - client := mockBuilder(). - Route("GET /v1/domains/1/nameservers", - servermock.JSONEncode(NameserverResponse{ - Nameservers: []*Nameserver{ - {Name: ns1}, - {Name: ns2}, - // {Name: "ns.fake.de"}, - }, - })). - Build(t) + client, mux := setupTest(t) - err := client.CheckNameservers(t.Context(), 1) + mux.HandleFunc("/v1/domains/1/nameservers", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + err := checkAuthorizationHeader(req) + if err != nil { + http.Error(rw, err.Error(), http.StatusUnauthorized) + return + } + + nsResp := NameserverResponse{ + Nameservers: []*Nameserver{ + {Name: ns1}, + {Name: ns2}, + // {Name: "ns.fake.de"}, + }, + } + + err = json.NewEncoder(rw).Encode(nsResp) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + err := client.CheckNameservers(context.Background(), 1) require.NoError(t, err) } func TestClient_CreateRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /v1/domains/1/nameservers/records", nil, - servermock.CheckRequestJSONBodyFromFixture("create_record-request.json")). - Build(t) + client, mux := setupTest(t) + + mux.HandleFunc("/v1/domains/1/nameservers/records", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + err := checkAuthorizationHeader(req) + if err != nil { + http.Error(rw, err.Error(), http.StatusUnauthorized) + return + } + + content, err := io.ReadAll(req.Body) + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + if string(bytes.TrimSpace(content)) != `{"name":"test.com","value":"value","ttl":300,"priority":0,"type":"TXT"}` { + http.Error(rw, "invalid request body: "+string(content), http.StatusBadRequest) + return + } + }) record := &Record{ Name: "test.com", @@ -69,51 +141,121 @@ func TestClient_CreateRecord(t *testing.T) { Value: "value", } - err := client.CreateRecord(t.Context(), 1, record) + err := client.CreateRecord(context.Background(), 1, record) require.NoError(t, err) } func TestClient_DeleteTXTRecord(t *testing.T) { + client, mux := setupTest(t) + domainName := "lego.test" recordValue := "test" - client := mockBuilder(). - Route("GET /v1/domains/", - servermock.JSONEncode(DomainResponse{ - ID: 1, - Name: domainName, - })). - Route("GET /v1/domains/1/nameservers", - servermock.JSONEncode(NameserverResponse{ - Nameservers: []*Nameserver{{Name: ns1}, {Name: ns2}}, - })). - Route("GET /v1/domains/1/nameservers/records", - servermock.JSONEncode(RecordListingResponse{ + records := []*Record{ + { + Name: "_acme-challenge", + Value: recordValue, + Type: "TXT", + }, + { + Name: "_acme-challenge", + Value: recordValue, + Type: "A", + }, + { + Name: "foobar", + Value: recordValue, + Type: "TXT", + }, + } + + expectedRecords := []*Record{ + { + Name: "_acme-challenge", + Value: recordValue, + Type: "A", + }, + { + Name: "foobar", + Value: recordValue, + Type: "TXT", + }, + } + + mux.HandleFunc("/v1/domains/1", func(rw http.ResponseWriter, req *http.Request) { + err := checkAuthorizationHeader(req) + if err != nil { + http.Error(rw, err.Error(), http.StatusUnauthorized) + return + } + + resp := DomainResponse{ + ID: 1, + Name: domainName, + } + + err = json.NewEncoder(rw).Encode(resp) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + mux.HandleFunc("/v1/domains/1/nameservers", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + nsResp := NameserverResponse{ + Nameservers: []*Nameserver{{Name: ns1}, {Name: ns2}}, + } + + err := json.NewEncoder(rw).Encode(nsResp) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + mux.HandleFunc("/v1/domains/1/nameservers/records", func(rw http.ResponseWriter, req *http.Request) { + switch req.Method { + case http.MethodGet: + resp := RecordListingResponse{ Embedded: EmbeddedRecordList{ - Records: []*Record{ - { - Name: "_acme-challenge", - Value: recordValue, - Type: "TXT", - }, - { - Name: "_acme-challenge", - Value: recordValue, - Type: "A", - }, - { - Name: "foobar", - Value: recordValue, - Type: "TXT", - }, - }, + Records: records, }, - })). - Route("PUT /v1/domains/1/nameservers/records", nil, - servermock.CheckRequestJSONBodyFromFixture("delete_txt_record-request.json")). - Build(t) + } + + err := json.NewEncoder(rw).Encode(resp) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + case http.MethodPut: + var records []*Record + err := json.NewDecoder(req.Body).Decode(&records) + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + if len(records) == 0 { + http.Error(rw, "empty request body", http.StatusBadRequest) + return + } + + if !reflect.DeepEqual(expectedRecords, records) { + http.Error(rw, fmt.Sprintf("invalid records: %v", records), http.StatusBadRequest) + return + } + default: + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + } + }) info := dns01.GetChallengeInfo(domainName, "abc") - err := client.DeleteTXTRecord(t.Context(), 1, info.EffectiveFQDN, recordValue) + err := client.DeleteTXTRecord(context.Background(), 1, info.EffectiveFQDN, recordValue) require.NoError(t, err) } diff --git a/providers/dns/checkdomain/internal/fixtures/create_record-request.json b/providers/dns/checkdomain/internal/fixtures/create_record-request.json deleted file mode 100644 index af1d50625..000000000 --- a/providers/dns/checkdomain/internal/fixtures/create_record-request.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "test.com", - "value": "value", - "ttl": 300, - "priority": 0, - "type": "TXT" -} diff --git a/providers/dns/checkdomain/internal/fixtures/delete_txt_record-request.json b/providers/dns/checkdomain/internal/fixtures/delete_txt_record-request.json deleted file mode 100644 index 67cb2570c..000000000 --- a/providers/dns/checkdomain/internal/fixtures/delete_txt_record-request.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "name": "_acme-challenge", - "value": "test", - "ttl": 0, - "priority": 0, - "type": "A" - }, - { - "name": "foobar", - "value": "test", - "ttl": 0, - "priority": 0, - "type": "TXT" - } -] diff --git a/providers/dns/civo/civo.go b/providers/dns/civo/civo.go index dfb7c307f..26bdc7995 100644 --- a/providers/dns/civo/civo.go +++ b/providers/dns/civo/civo.go @@ -2,17 +2,19 @@ package civo import ( - "context" "errors" "fmt" - "net/http" "time" - "github.com/go-acme/lego/v4/challenge" + "github.com/civo/civogo" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/civo/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" +) + +const ( + minTTL = 600 + defaultPollingInterval = 30 * time.Second + defaultPropagationTimeout = 300 * time.Second ) // Environment variables names. @@ -24,25 +26,15 @@ const ( EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const ( - minTTL = 600 - defaultPollingInterval = 30 * time.Second - defaultPropagationTimeout = 300 * time.Second -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { - Token string - + ProjectID string + Token string PropagationTimeout time.Duration PollingInterval time.Duration TTL int - HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. @@ -51,16 +43,13 @@ func NewDefaultConfig() *Config { TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, defaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, defaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config - client *internal.Client + client *civogo.Client } // NewDNSProvider returns a DNSProvider instance configured for CIVO. @@ -92,11 +81,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { } // Create a Civo client - DNS is region independent, we can use any region - client, err := internal.NewClient( - clientdebug.Wrap( - internal.OAuthStaticAccessToken(config.HTTPClient, config.Token), - ), - "LON1") + client, err := civogo.NewClient(config.Token, "LON1") if err != nil { return nil, fmt.Errorf("civo: %w", err) } @@ -108,8 +93,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) - ctx := context.Background() - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { return fmt.Errorf("civo: could not find zone for domain %q: %w", domain, err) @@ -117,7 +100,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { zone := dns01.UnFqdn(authZone) - domainID, err := d.getDomainIDByName(ctx, zone) + dnsDomain, err := d.client.GetDNSDomain(zone) if err != nil { return fmt.Errorf("civo: %w", err) } @@ -127,10 +110,10 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("civo: %w", err) } - _, err = d.client.CreateDNSRecord(ctx, domainID, internal.Record{ + _, err = d.client.CreateDNSRecord(dnsDomain.ID, &civogo.DNSRecordConfig{ Name: subDomain, Value: info.Value, - Type: "TXT", + Type: civogo.DNSRecordTypeTXT, TTL: d.config.TTL, }) if err != nil { @@ -144,8 +127,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) - ctx := context.Background() - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { return fmt.Errorf("civo: could not find zone for domain %q: %w", domain, err) @@ -153,12 +134,12 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { zone := dns01.UnFqdn(authZone) - domainID, err := d.getDomainIDByName(ctx, zone) + dnsDomain, err := d.client.GetDNSDomain(zone) if err != nil { return fmt.Errorf("civo: %w", err) } - dnsRecords, err := d.client.ListDNSRecords(ctx, domainID) + dnsRecords, err := d.client.ListDNSRecords(dnsDomain.ID) if err != nil { return fmt.Errorf("civo: %w", err) } @@ -168,8 +149,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("civo: %w", err) } - var dnsRecord internal.Record - + var dnsRecord civogo.DNSRecord for _, entry := range dnsRecords { if entry.Name == subDomain && entry.Value == info.Value { dnsRecord = entry @@ -177,7 +157,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } } - err = d.client.DeleteDNSRecord(ctx, dnsRecord) + _, err = d.client.DeleteDNSRecord(&dnsRecord) if err != nil { return fmt.Errorf("civo: %w", err) } @@ -190,18 +170,3 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } - -func (d *DNSProvider) getDomainIDByName(ctx context.Context, domain string) (string, error) { - domains, err := d.client.ListDomains(ctx) - if err != nil { - return "", fmt.Errorf("list domains: %w", err) - } - - for _, d := range domains { - if d.Name == domain { - return d.ID, nil - } - } - - return "", fmt.Errorf("domain %q not found", domain) -} diff --git a/providers/dns/civo/civo.toml b/providers/dns/civo/civo.toml index b525712c8..9b759dc8c 100644 --- a/providers/dns/civo/civo.toml +++ b/providers/dns/civo/civo.toml @@ -6,16 +6,16 @@ Since = "v4.9.0" Example = ''' CIVO_TOKEN=xxxxxx \ -lego --dns civo -d '*.example.com' -d example.com run +lego --email you@example.com --dns civo --domains my.example.org run ''' [Configuration] - [Configuration.Credentials] - CIVO_TOKEN = "Authentication token" - [Configuration.Additional] - CIVO_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 30)" - CIVO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 300)" - CIVO_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)" + [Configuration.Credentials] + CIVO_TOKEN = "Authentication token" + [Configuration.Additional] + CIVO_POLLING_INTERVAL = "Time between DNS propagation check" + CIVO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + CIVO_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://www.civo.com/api/dns" diff --git a/providers/dns/civo/civo_test.go b/providers/dns/civo/civo_test.go index 416dbac1d..333cf0b1f 100644 --- a/providers/dns/civo/civo_test.go +++ b/providers/dns/civo/civo_test.go @@ -2,13 +2,10 @@ package civo import ( "fmt" - "net/http/httptest" - "net/url" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -42,7 +39,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -107,7 +103,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -121,7 +116,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -130,66 +124,3 @@ func TestLiveCleanUp(t *testing.T) { err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.HTTPClient = server.Client() - config.Token = "secret" - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - p.client.BaseURL, _ = url.Parse(server.URL) - - return p, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(). - With("Authorization", "Bearer secret"). - WithRegexp("User-Agent", `goacme-lego/[0-9.]+ \(.+\)`), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - // https://www.civo.com/api/dns#list-domain-names - Route("GET /dns", - servermock.ResponseFromInternal("list_domain_names.json"), - servermock.CheckQueryParameter().Strict(). - With("region", "LON1")). - // https://www.civo.com/api/dns#create-a-new-dns-record - Route("POST /dns/7088fcea-7658-43e6-97fa-273f901978fd/records", - servermock.ResponseFromInternal("create_dns_record.json"), - servermock.CheckRequestJSONBodyFromInternal("create_dns_record-request.json")). - Build(t) - - err := provider.Present("example.com", "abd", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - // https://www.civo.com/api/dns#list-domain-names - Route("GET /dns", - servermock.ResponseFromInternal("list_domain_names.json"), - servermock.CheckQueryParameter(). - With("region", "LON1")). - // https://www.civo.com/api/dns#list-dns-records - Route("GET /dns/7088fcea-7658-43e6-97fa-273f901978fd/records", - servermock.ResponseFromInternal("list_dns_records.json"), - servermock.CheckQueryParameter().Strict(). - With("region", "LON1")). - // https://www.civo.com/api/dns#deleting-a-dns-record - Route("DELETE /dns/edc5dacf-a2ad-4757-41ee-c12f06259c70/records/76cc107f-fbef-4e2b-b97f-f5d34f4075d3", - servermock.ResponseFromInternal("delete_dns_record.json"), - servermock.CheckQueryParameter().Strict(). - With("region", "LON1")). - Build(t) - - err := provider.CleanUp("example.com", "abd", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/civo/internal/client.go b/providers/dns/civo/internal/client.go deleted file mode 100644 index dc1d57793..000000000 --- a/providers/dns/civo/internal/client.go +++ /dev/null @@ -1,213 +0,0 @@ -/* -Package internal Civo API client. - -Because the dependencies on k8s, the official client cannot be used. -- https://github.com/civo/civogo/blob/v0.2.99/go.mod -> k8s.io/client-go -- https://github.com/civo/civogo/blob/v0.3.34/go.mod -> k8s.io/api -- https://github.com/civo/civogo/blob/v0.3.38/go.mod -> k8s.io/api + k8s.io/apimachinery -- Current version -> https://github.com/civo/civogo/blob/v0.6.1/go.mod -*/ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" - "golang.org/x/oauth2" -) - -const defaultBaseURL = "https://api.civo.com/v2" - -// Client the Civo API client. -type Client struct { - region string - - BaseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(hc *http.Client, region string) (*Client, error) { - baseURL, _ := url.Parse(defaultBaseURL) - - if hc == nil { - hc = &http.Client{Timeout: 10 * time.Second} - } - - return &Client{ - region: region, - BaseURL: baseURL, - HTTPClient: hc, - }, nil -} - -// ListDomains a list of all domain names within the account. -// https://www.civo.com/api/dns#list-domain-names -func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) { - endpoint := c.BaseURL.JoinPath("dns") - - req, err := c.newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var result []Domain - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return result, nil -} - -// ListDNSRecords a list of all DNS records in the specified domain. -// https://www.civo.com/api/dns#list-dns-records -func (c *Client) ListDNSRecords(ctx context.Context, domainID string) ([]Record, error) { - endpoint := c.BaseURL.JoinPath("dns", domainID, "records") - - req, err := c.newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var result []Record - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return result, nil -} - -// CreateDNSRecord creates DNS records for a specific domain. -// https://www.civo.com/api/dns#create-a-new-dns-record -func (c *Client) CreateDNSRecord(ctx context.Context, domainID string, record Record) (*Record, error) { - endpoint := c.BaseURL.JoinPath("dns", domainID, "records") - - req, err := c.newJSONRequest(ctx, http.MethodPost, endpoint, record) - if err != nil { - return nil, err - } - - var result Record - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return &result, nil -} - -// DeleteDNSRecord remove a DNS record from a domain. -// https://www.civo.com/api/dns#deleting-a-dns-record -func (c *Client) DeleteDNSRecord(ctx context.Context, record Record) error { - endpoint := c.BaseURL.JoinPath("dns", record.DomainID, "records", record.ID) - - req, err := c.newJSONRequest(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return err - } - - return c.do(req, nil) -} - -func (c *Client) do(req *http.Request, result any) error { - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func (c *Client) newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - if method == http.MethodGet || method == http.MethodDelete { - query := endpoint.Query() - query.Set("region", c.region) - - endpoint.RawQuery = query.Encode() - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - useragent.SetHeader(req.Header) - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIError - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return &errAPI -} - -// OAuthStaticAccessToken Authorization header. -// https://www.civo.com/api#authentication -func OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client { - if client == nil { - client = &http.Client{Timeout: 5 * time.Second} - } - - client.Transport = &oauth2.Transport{ - Source: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}), - Base: client.Transport, - } - - return client -} diff --git a/providers/dns/civo/internal/client_test.go b/providers/dns/civo/internal/client_test.go deleted file mode 100644 index ad56b75de..000000000 --- a/providers/dns/civo/internal/client_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient(OAuthStaticAccessToken(server.Client(), "secret"), "LON1") - if err != nil { - return nil, err - } - - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(). - With("Authorization", "Bearer secret"). - WithRegexp("User-Agent", `goacme-lego/[0-9.]+ \(.+\)`), - ) -} - -func TestClient_ListDomains(t *testing.T) { - client := mockBuilder(). - Route("GET /dns", - servermock.ResponseFromFixture("list_domain_names.json"), - servermock.CheckQueryParameter().Strict(). - With("region", "LON1")). - Build(t) - - domains, err := client.ListDomains(t.Context()) - require.NoError(t, err) - - expected := []Domain{{ - ID: "7088fcea-7658-43e6-97fa-273f901978fd", - AccountID: "e7e8386e-434e-482f-95e0-c406e5d564c2", - Name: "example.com", - }} - - assert.Equal(t, expected, domains) -} - -func TestClient_ListDNSRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/7088fcea-7658-43e6-97fa-273f901978fd/records", - servermock.ResponseFromFixture("list_dns_records.json"), - servermock.CheckQueryParameter().Strict(). - With("region", "LON1")). - Build(t) - - records, err := client.ListDNSRecords(t.Context(), "7088fcea-7658-43e6-97fa-273f901978fd") - require.NoError(t, err) - - expected := []Record{ - { - ID: "76cc107f-fbef-4e2b-b97f-f5d34f4075d3", - DomainID: "edc5dacf-a2ad-4757-41ee-c12f06259c70", - Name: "_acme-challenge", - Value: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - Type: "txt", - TTL: 600, - }, - } - - assert.Equal(t, expected, records) -} - -func TestClient_ListDNSRecords_error(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/7088fcea-7658-43e6-97fa-273f901978fd/records", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) - - _, err := client.ListDNSRecords(t.Context(), "7088fcea-7658-43e6-97fa-273f901978fd") - require.EqualError(t, err, "database_account_not_found: Failed to find the account within the internal database") -} - -func TestClient_ListDNSRecords_error_raw(t *testing.T) { - // the API says: - // > 4xx/5xx status may not be JSON, unless it's obvious that the response should be parsed for a specific reason. - // > So, for example, 404 Not Found pages are a standard page of text - // > but 403 Unauthorized requests may have a reason attribute available in the JSON object. - // https://www.civo.com/api#parameters-and-responses - client := mockBuilder(). - Route("GET /dns/7088fcea-7658-43e6-97fa-273f901978fd/records", - servermock.RawStringResponse(http.StatusText(http.StatusNotFound)). - WithStatusCode(http.StatusNotFound)). - Build(t) - - _, err := client.ListDNSRecords(t.Context(), "7088fcea-7658-43e6-97fa-273f901978fd") - require.EqualError(t, err, "unexpected status code: [status code: 404] body: Not Found") -} - -func TestClient_CreateDNSRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /dns/7088fcea-7658-43e6-97fa-273f901978fd/records", - servermock.ResponseFromFixture("create_dns_record.json"), - servermock.CheckRequestJSONBodyFromFixture("create_dns_record-request.json")). - Build(t) - - record := Record{ - Name: "_acme-challenge", - Value: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - Type: "TXT", - TTL: 600, - } - - newRecord, err := client.CreateDNSRecord(t.Context(), "7088fcea-7658-43e6-97fa-273f901978fd", record) - require.NoError(t, err) - - expected := &Record{ - ID: "76cc107f-fbef-4e2b-b97f-f5d34f4075d3", - DomainID: "edc5dacf-a2ad-4757-41ee-c12f06259c70", - Name: "_acme-challenge", - Value: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - Type: "txt", - TTL: 600, - } - - assert.Equal(t, expected, newRecord) -} - -func TestClient_DeleteDNSRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /dns/edc5dacf-a2ad-4757-41ee-c12f06259c70/records/76cc107f-fbef-4e2b-b97f-f5d34f4075d3", - servermock.ResponseFromFixture("delete_dns_record.json"), - servermock.CheckQueryParameter().Strict(). - With("region", "LON1")). - Build(t) - - record := Record{ - ID: "76cc107f-fbef-4e2b-b97f-f5d34f4075d3", - DomainID: "edc5dacf-a2ad-4757-41ee-c12f06259c70", - Name: "_acme-challenge", - Value: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - Type: "TXT", - TTL: 600, - } - - err := client.DeleteDNSRecord(t.Context(), record) - require.NoError(t, err) -} diff --git a/providers/dns/civo/internal/fixtures/create_dns_record-request.json b/providers/dns/civo/internal/fixtures/create_dns_record-request.json deleted file mode 100644 index ec881e142..000000000 --- a/providers/dns/civo/internal/fixtures/create_dns_record-request.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "type": "TXT", - "name": "_acme-challenge", - "value": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "ttl": 600 -} diff --git a/providers/dns/civo/internal/fixtures/create_dns_record.json b/providers/dns/civo/internal/fixtures/create_dns_record.json deleted file mode 100644 index d9557cf23..000000000 --- a/providers/dns/civo/internal/fixtures/create_dns_record.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "id": "76cc107f-fbef-4e2b-b97f-f5d34f4075d3", - "created_at": "2019-04-11T12:47:56.000+01:00", - "updated_at": "2019-04-11T12:47:56.000+01:00", - "account_id": null, - "domain_id": "edc5dacf-a2ad-4757-41ee-c12f06259c70", - "name": "_acme-challenge", - "value": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "type": "txt", - "ttl": 600 -} diff --git a/providers/dns/civo/internal/fixtures/delete_dns_record.json b/providers/dns/civo/internal/fixtures/delete_dns_record.json deleted file mode 100644 index 80bf76ad5..000000000 --- a/providers/dns/civo/internal/fixtures/delete_dns_record.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "result": "success" -} diff --git a/providers/dns/civo/internal/fixtures/error.json b/providers/dns/civo/internal/fixtures/error.json deleted file mode 100644 index 0a55e079f..000000000 --- a/providers/dns/civo/internal/fixtures/error.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "code": "database_account_not_found", - "reason": "Failed to find the account within the internal database" -} diff --git a/providers/dns/civo/internal/fixtures/list_dns_records.json b/providers/dns/civo/internal/fixtures/list_dns_records.json deleted file mode 100644 index 0c4e54737..000000000 --- a/providers/dns/civo/internal/fixtures/list_dns_records.json +++ /dev/null @@ -1,13 +0,0 @@ -[ - { - "id": "76cc107f-fbef-4e2b-b97f-f5d34f4075d3", - "created_at": "2019-04-11T12:47:56.000+01:00", - "updated_at": "2019-04-11T12:47:56.000+01:00", - "account_id": null, - "domain_id": "edc5dacf-a2ad-4757-41ee-c12f06259c70", - "name": "_acme-challenge", - "value": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "type": "txt", - "ttl": 600 - } -] diff --git a/providers/dns/civo/internal/fixtures/list_domain_names.json b/providers/dns/civo/internal/fixtures/list_domain_names.json deleted file mode 100644 index 909cdca04..000000000 --- a/providers/dns/civo/internal/fixtures/list_domain_names.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "id": "7088fcea-7658-43e6-97fa-273f901978fd", - "account_id": "e7e8386e-434e-482f-95e0-c406e5d564c2", - "name": "example.com" - } -] diff --git a/providers/dns/civo/internal/types.go b/providers/dns/civo/internal/types.go deleted file mode 100644 index d173e2fcd..000000000 --- a/providers/dns/civo/internal/types.go +++ /dev/null @@ -1,28 +0,0 @@ -package internal - -import "fmt" - -type APIError struct { - Code string `json:"code"` - Reason string `json:"reason"` -} - -func (a *APIError) Error() string { - return fmt.Sprintf("%s: %s", a.Code, a.Reason) -} - -type Record struct { - ID string `json:"id,omitempty"` - AccountID string `json:"account_id,omitempty"` - DomainID string `json:"domain_id,omitempty"` - Name string `json:"name,omitempty"` - Value string `json:"value,omitempty"` - Type string `json:"type,omitempty"` - TTL int `json:"ttl,omitempty"` -} - -type Domain struct { - ID string `json:"id,omitempty"` - AccountID string `json:"account_id,omitempty"` - Name string `json:"name,omitempty"` -} diff --git a/providers/dns/clouddns/clouddns.go b/providers/dns/clouddns/clouddns.go index 77b673738..c3b13887e 100644 --- a/providers/dns/clouddns/clouddns.go +++ b/providers/dns/clouddns/clouddns.go @@ -8,11 +8,9 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/clouddns/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -29,8 +27,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the DNSProvider. type Config struct { ClientID string @@ -94,8 +90,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{client: client, config: config}, nil } diff --git a/providers/dns/clouddns/clouddns.toml b/providers/dns/clouddns/clouddns.toml index 6f516e834..3c73dd99f 100644 --- a/providers/dns/clouddns/clouddns.toml +++ b/providers/dns/clouddns/clouddns.toml @@ -8,7 +8,7 @@ Example = ''' CLOUDDNS_CLIENT_ID=bLsdFAks23429841238feb177a572aX \ CLOUDDNS_EMAIL=you@example.com \ CLOUDDNS_PASSWORD=b9841238feb177a84330f \ -lego --dns clouddns -d '*.example.com' -d example.com run +lego --email you@example.com --dns clouddns --domains my.example.org run ''' [Configuration] @@ -17,10 +17,10 @@ lego --dns clouddns -d '*.example.com' -d example.com run CLOUDDNS_EMAIL = "Account email" CLOUDDNS_PASSWORD = "Account password" [Configuration.Additional] - CLOUDDNS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 5)" - CLOUDDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - CLOUDDNS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - CLOUDDNS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + CLOUDDNS_POLLING_INTERVAL = "Time between DNS propagation check" + CLOUDDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + CLOUDDNS_TTL = "The TTL of the TXT record used for the DNS challenge" + CLOUDDNS_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://admin.vshosting.cloud/clouddns/swagger/" diff --git a/providers/dns/clouddns/clouddns_test.go b/providers/dns/clouddns/clouddns_test.go index f1e2a196e..d7bfc4a1f 100644 --- a/providers/dns/clouddns/clouddns_test.go +++ b/providers/dns/clouddns/clouddns_test.go @@ -63,7 +63,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -149,7 +148,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -163,7 +161,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/clouddns/internal/client.go b/providers/dns/clouddns/internal/client.go index 9fb6902de..cd3da50c7 100644 --- a/providers/dns/clouddns/internal/client.go +++ b/providers/dns/clouddns/internal/client.go @@ -122,7 +122,6 @@ func (c *Client) getDomain(ctx context.Context, zone string) (Domain, error) { } var result SearchResponse - err = c.do(req, &result) if err != nil { return Domain{}, err @@ -144,7 +143,6 @@ func (c *Client) getRecord(ctx context.Context, domainID, recordName string) (Re } var result DomainInfo - err = c.do(req, &result) if err != nil { return Record{}, err @@ -234,7 +232,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) var response APIError - err := json.Unmarshal(raw, &response) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/clouddns/internal/client_test.go b/providers/dns/clouddns/internal/client_test.go index a5b780e42..2a4891cce 100644 --- a/providers/dns/clouddns/internal/client_test.go +++ b/providers/dns/clouddns/internal/client_test.go @@ -1,65 +1,130 @@ package internal import ( + "context" + "encoding/json" + "net/http" "net/http/httptest" "net/url" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("clientID", "email@example.com", "secret", 300) - client.HTTPClient = server.Client() - client.apiBaseURL, _ = url.Parse(server.URL + "/api") - client.loginURL, _ = url.Parse(server.URL + "/login") +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(), - ) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient("clientID", "email@example.com", "secret", 300) + client.HTTPClient = server.Client() + client.apiBaseURL, _ = url.Parse(server.URL + "/api") + client.loginURL, _ = url.Parse(server.URL + "/login") + + return client, mux } func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /api/domain/search", - servermock.ResponseFromFixture("domain_search.json"), - servermock.CheckRequestJSONBodyFromFixture("domain_search-request.json")). - Route("POST /api/record-txt", nil, - servermock.CheckRequestJSONBodyFromFixture("record_txt-request.json")). - Route("PUT /api/domain/A/publish", nil, - servermock.CheckRequestJSONBodyFromFixture("publish-request.json")). - Route("POST /login", - servermock.ResponseFromFixture("login.json"), - servermock.CheckRequestJSONBodyFromFixture("login-request.json")). - Build(t) + client, mux := setupTest(t) - ctx, err := client.CreateAuthenticatedContext(t.Context()) - require.NoError(t, err) + mux.HandleFunc("/api/domain/search", func(rw http.ResponseWriter, req *http.Request) { + response := SearchResponse{ + Items: []Domain{ + { + ID: "A", + DomainName: "example.com", + }, + }, + } - err = client.AddRecord(ctx, "example.com", "_acme-challenge.example.com", "txt") + err := json.NewEncoder(rw).Encode(response) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + mux.HandleFunc("/api/record-txt", func(rw http.ResponseWriter, req *http.Request) {}) + mux.HandleFunc("/api/domain/A/publish", func(rw http.ResponseWriter, req *http.Request) {}) + mux.HandleFunc("/login", func(rw http.ResponseWriter, req *http.Request) { + response := AuthResponse{ + Auth: Auth{ + AccessToken: "at", + RefreshToken: "", + }, + } + + err := json.NewEncoder(rw).Encode(response) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + err := client.AddRecord(context.Background(), "example.com", "_acme-challenge.example.com", "txt") require.NoError(t, err) } func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /api/domain/search", - servermock.ResponseFromFixture("domain_search.json"), - servermock.CheckRequestJSONBodyFromFixture("domain_search-request.json")). - Route("GET /api/domain/A", - servermock.ResponseFromFixture("domain-request.json")). - Route("DELETE /api/record/R01", nil). - Route("PUT /api/domain/A/publish", nil, - servermock.CheckRequestJSONBodyFromFixture("publish-request.json")). - Route("POST /login", - servermock.ResponseFromFixture("login.json"), - servermock.CheckRequestJSONBodyFromFixture("login-request.json")). - Build(t) + client, mux := setupTest(t) - ctx, err := client.CreateAuthenticatedContext(t.Context()) + mux.HandleFunc("/api/domain/search", func(rw http.ResponseWriter, req *http.Request) { + response := SearchResponse{ + Items: []Domain{ + { + ID: "A", + DomainName: "example.com", + }, + }, + } + + err := json.NewEncoder(rw).Encode(response) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + mux.HandleFunc("/api/domain/A", func(rw http.ResponseWriter, req *http.Request) { + response := DomainInfo{ + ID: "Z", + DomainName: "example.com", + LastDomainRecordList: []Record{ + { + ID: "R01", + DomainID: "A", + Name: "_acme-challenge.example.com", + Value: "txt", + Type: "TXT", + }, + }, + SoaTTL: 300, + } + + err := json.NewEncoder(rw).Encode(response) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + mux.HandleFunc("/api/record/R01", func(rw http.ResponseWriter, req *http.Request) {}) + mux.HandleFunc("/api/domain/A/publish", func(rw http.ResponseWriter, req *http.Request) {}) + mux.HandleFunc("/login", func(rw http.ResponseWriter, req *http.Request) { + response := AuthResponse{ + Auth: Auth{ + AccessToken: "at", + RefreshToken: "", + }, + } + + err := json.NewEncoder(rw).Encode(response) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + ctx, err := client.CreateAuthenticatedContext(context.Background()) require.NoError(t, err) err = client.DeleteRecord(ctx, "example.com", "_acme-challenge.example.com") diff --git a/providers/dns/clouddns/internal/fixtures/domain-request.json b/providers/dns/clouddns/internal/fixtures/domain-request.json deleted file mode 100644 index 00f60b9bd..000000000 --- a/providers/dns/clouddns/internal/fixtures/domain-request.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "id": "Z", - "domainName": "example.com", - "lastDomainRecordList": [ - { - "id": "R01", - "domainId": "A", - "name": "_acme-challenge.example.com", - "value": "txt", - "type": "TXT" - } - ], - "soaTtl": 300 -} diff --git a/providers/dns/clouddns/internal/fixtures/domain_search-request.json b/providers/dns/clouddns/internal/fixtures/domain_search-request.json deleted file mode 100644 index 89043dc3a..000000000 --- a/providers/dns/clouddns/internal/fixtures/domain_search-request.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "search": [ - { - "name": "clientId", - "operator": "eq", - "value": "clientID" - }, - { - "name": "domainName", - "operator": "eq", - "value": "example.com" - } - ] -} diff --git a/providers/dns/clouddns/internal/fixtures/domain_search.json b/providers/dns/clouddns/internal/fixtures/domain_search.json deleted file mode 100644 index 4ee454732..000000000 --- a/providers/dns/clouddns/internal/fixtures/domain_search.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "items": [ - { - "id": "A", - "domainName": "example.com" - } - ] -} diff --git a/providers/dns/clouddns/internal/fixtures/login-request.json b/providers/dns/clouddns/internal/fixtures/login-request.json deleted file mode 100644 index 132577e6b..000000000 --- a/providers/dns/clouddns/internal/fixtures/login-request.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "email": "email@example.com", - "password": "secret" -} diff --git a/providers/dns/clouddns/internal/fixtures/login.json b/providers/dns/clouddns/internal/fixtures/login.json deleted file mode 100644 index e72ffb19b..000000000 --- a/providers/dns/clouddns/internal/fixtures/login.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "auth": { - "accessToken": "at" - } -} diff --git a/providers/dns/clouddns/internal/fixtures/publish-request.json b/providers/dns/clouddns/internal/fixtures/publish-request.json deleted file mode 100644 index 383e26958..000000000 --- a/providers/dns/clouddns/internal/fixtures/publish-request.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "soaTtl": 300 -} diff --git a/providers/dns/clouddns/internal/fixtures/record_txt-request.json b/providers/dns/clouddns/internal/fixtures/record_txt-request.json deleted file mode 100644 index cbc2a32a0..000000000 --- a/providers/dns/clouddns/internal/fixtures/record_txt-request.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "domainId": "A", - "name": "_acme-challenge.example.com", - "value": "txt", - "type": "TXT" -} diff --git a/providers/dns/clouddns/internal/identity.go b/providers/dns/clouddns/internal/identity.go index 6b20ad814..4ea5c5049 100644 --- a/providers/dns/clouddns/internal/identity.go +++ b/providers/dns/clouddns/internal/identity.go @@ -20,7 +20,6 @@ func (c *Client) login(ctx context.Context) (*AuthResponse, error) { } var result AuthResponse - err = c.do(req, &result) if err != nil { return nil, err diff --git a/providers/dns/clouddns/internal/identity_test.go b/providers/dns/clouddns/internal/identity_test.go index 267f73335..3c727448d 100644 --- a/providers/dns/clouddns/internal/identity_test.go +++ b/providers/dns/clouddns/internal/identity_test.go @@ -1,22 +1,41 @@ package internal import ( + "context" + "encoding/json" + "net/http" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestClient_CreateAuthenticatedContext(t *testing.T) { - client := mockBuilder(). - Route("POST /login", - servermock.ResponseFromFixture("login.json"), - servermock.CheckRequestJSONBodyFromFixture("login-request.json")). - Route("DELETE /api/record/xxx", nil). - Build(t) + client, mux := setupTest(t) - ctx, err := client.CreateAuthenticatedContext(t.Context()) + mux.HandleFunc("/login", func(rw http.ResponseWriter, req *http.Request) { + response := AuthResponse{ + Auth: Auth{ + AccessToken: "at", + RefreshToken: "", + }, + } + + err := json.NewEncoder(rw).Encode(response) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + mux.HandleFunc("/api/record/xxx", func(rw http.ResponseWriter, req *http.Request) { + authorization := req.Header.Get(authorizationHeader) + if authorization != "Bearer at" { + http.Error(rw, "invalid credential: "+authorization, http.StatusUnauthorized) + return + } + }) + + ctx, err := client.CreateAuthenticatedContext(context.Background()) require.NoError(t, err) at := getAccessToken(ctx) diff --git a/providers/dns/clouddns/internal/types.go b/providers/dns/clouddns/internal/types.go index 9de11d848..a53c958a7 100644 --- a/providers/dns/clouddns/internal/types.go +++ b/providers/dns/clouddns/internal/types.go @@ -21,7 +21,7 @@ type Authorization struct { } type AuthResponse struct { - Auth Auth `json:"auth"` + Auth Auth `json:"auth,omitempty"` } type Auth struct { diff --git a/providers/dns/cloudflare/cloudflare.go b/providers/dns/cloudflare/cloudflare.go index 98b3495bb..efdbd6e7a 100644 --- a/providers/dns/cloudflare/cloudflare.go +++ b/providers/dns/cloudflare/cloudflare.go @@ -6,48 +6,19 @@ import ( "errors" "fmt" "net/http" - "strconv" - "strings" "sync" "time" - "github.com/go-acme/lego/v4/challenge" + "github.com/cloudflare/cloudflare-go" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/cloudflare/internal" -) - -// Environment variables names. -const ( - envNamespace = "CLOUDFLARE_" - - EnvEmail = envNamespace + "EMAIL" - EnvAPIKey = envNamespace + "API_KEY" - - EnvDNSAPIToken = envNamespace + "DNS_API_TOKEN" - EnvZoneAPIToken = envNamespace + "ZONE_API_TOKEN" - - EnvBaseURL = envNamespace + "BASE_URL" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -const ( - altEnvNamespace = "CF_" - - altEnvEmail = altEnvNamespace + "API_EMAIL" ) const ( minTTL = 120 ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { AuthEmail string @@ -56,8 +27,6 @@ type Config struct { AuthToken string ZoneToken string - BaseURL string - TTL int PropagationTimeout time.Duration PollingInterval time.Duration @@ -67,11 +36,11 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOneWithFallback(EnvTTL, minTTL, strconv.Atoi, altEnvName(EnvTTL)), - PropagationTimeout: env.GetOneWithFallback(EnvPropagationTimeout, 2*time.Minute, env.ParseSecond, altEnvName(EnvPropagationTimeout)), - PollingInterval: env.GetOneWithFallback(EnvPollingInterval, dns01.DefaultPollingInterval, env.ParseSecond, altEnvName(EnvPollingInterval)), + TTL: env.GetOrDefaultInt("CLOUDFLARE_TTL", minTTL), + PropagationTimeout: env.GetOrDefaultSecond("CLOUDFLARE_PROPAGATION_TIMEOUT", 2*time.Minute), + PollingInterval: env.GetOrDefaultSecond("CLOUDFLARE_POLLING_INTERVAL", 2*time.Second), HTTPClient: &http.Client{ - Timeout: env.GetOneWithFallback(EnvHTTPTimeout, 30*time.Second, env.ParseSecond, altEnvName(EnvHTTPTimeout)), + Timeout: env.GetOrDefaultSecond("CLOUDFLARE_HTTP_TIMEOUT", 30*time.Second), }, } } @@ -99,15 +68,14 @@ type DNSProvider struct { // in this case pass both CLOUDFLARE_ZONE_API_TOKEN and CLOUDFLARE_DNS_API_TOKEN accordingly. func NewDNSProvider() (*DNSProvider, error) { values, err := env.GetWithFallback( - []string{EnvEmail, altEnvEmail}, - []string{EnvAPIKey, altEnvName(EnvAPIKey)}, + []string{"CLOUDFLARE_EMAIL", "CF_API_EMAIL"}, + []string{"CLOUDFLARE_API_KEY", "CF_API_KEY"}, ) if err != nil { var errT error - values, errT = env.GetWithFallback( - []string{EnvDNSAPIToken, altEnvName(EnvDNSAPIToken)}, - []string{EnvZoneAPIToken, altEnvName(EnvZoneAPIToken), EnvDNSAPIToken, altEnvName(EnvDNSAPIToken)}, + []string{"CLOUDFLARE_DNS_API_TOKEN", "CF_DNS_API_TOKEN"}, + []string{"CLOUDFLARE_ZONE_API_TOKEN", "CF_ZONE_API_TOKEN", "CLOUDFLARE_DNS_API_TOKEN", "CF_DNS_API_TOKEN"}, ) if errT != nil { //nolint:errorlint @@ -116,11 +84,10 @@ func NewDNSProvider() (*DNSProvider, error) { } config := NewDefaultConfig() - config.AuthEmail = values[EnvEmail] - config.AuthKey = values[EnvAPIKey] - config.AuthToken = values[EnvDNSAPIToken] - config.ZoneToken = values[EnvZoneAPIToken] - config.BaseURL = env.GetOrFile(EnvBaseURL) + config.AuthEmail = values["CLOUDFLARE_EMAIL"] + config.AuthKey = values["CLOUDFLARE_API_KEY"] + config.AuthToken = values["CLOUDFLARE_DNS_API_TOKEN"] + config.ZoneToken = values["CLOUDFLARE_ZONE_API_TOKEN"] return NewDNSProviderConfig(config) } @@ -155,8 +122,6 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) @@ -164,19 +129,19 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("cloudflare: could not find zone for domain %q: %w", domain, err) } - zoneID, err := d.client.ZoneIDByName(ctx, authZone) + zoneID, err := d.client.ZoneIDByName(authZone) if err != nil { return fmt.Errorf("cloudflare: failed to find zone %s: %w", authZone, err) } - dnsRecord := internal.Record{ + dnsRecord := cloudflare.CreateDNSRecordParams{ Type: "TXT", Name: dns01.UnFqdn(info.EffectiveFQDN), - Content: `"` + info.Value + `"`, + Content: info.Value, TTL: d.config.TTL, } - response, err := d.client.CreateDNSRecord(ctx, zoneID, dnsRecord) + response, err := d.client.CreateDNSRecord(context.Background(), zoneID, dnsRecord) if err != nil { return fmt.Errorf("cloudflare: failed to create TXT record: %w", err) } @@ -192,8 +157,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) @@ -201,7 +164,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("cloudflare: could not find zone for domain %q: %w", domain, err) } - zoneID, err := d.client.ZoneIDByName(ctx, authZone) + zoneID, err := d.client.ZoneIDByName(authZone) if err != nil { return fmt.Errorf("cloudflare: failed to find zone %s: %w", authZone, err) } @@ -210,14 +173,13 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() - if !ok { return fmt.Errorf("cloudflare: unknown record ID for '%s'", info.EffectiveFQDN) } - err = d.client.DeleteDNSRecord(ctx, zoneID, recordID) + err = d.client.DeleteDNSRecord(context.Background(), zoneID, recordID) if err != nil { - log.Printf("cloudflare: failed to delete TXT record: %v", err) + log.Printf("cloudflare: failed to delete TXT record: %w", err) } // Delete record ID from map @@ -227,7 +189,3 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return nil } - -func altEnvName(v string) string { - return strings.ReplaceAll(v, envNamespace, altEnvNamespace) -} diff --git a/providers/dns/cloudflare/cloudflare.toml b/providers/dns/cloudflare/cloudflare.toml index c46130fe6..514790baf 100644 --- a/providers/dns/cloudflare/cloudflare.toml +++ b/providers/dns/cloudflare/cloudflare.toml @@ -7,12 +7,12 @@ Since = "v0.3.0" Example = ''' CLOUDFLARE_EMAIL=you@example.com \ CLOUDFLARE_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \ -lego --dns cloudflare -d '*.example.com' -d example.com run +lego --email you@example.com --dns cloudflare --domains my.example.org run # or CLOUDFLARE_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \ -lego --dns cloudflare -d '*.example.com' -d example.com run +lego --email you@example.com --dns cloudflare --domains my.example.org run ''' Additional = ''' @@ -46,13 +46,12 @@ Then pass the API token as `CF_DNS_API_TOKEN` to Lego. **Alternatively,** if you prefer a more strict set of privileges, you can split the access tokens: -* Create one with *Zone / Zone / Read* permissions and scope it to all your zones or just the individual zone you need to edit. +* Create one with *Zone / Zone / Read* permissions and scope it to all your zones. This is needed to resolve domain names to Zone IDs and can be shared among multiple Lego installations. Pass this API token as `CF_ZONE_API_TOKEN` to Lego. * Create another API token with *Zone / DNS / Edit* permissions and set the scope to the domains you want to manage with a single Lego installation. Pass this token as `CF_DNS_API_TOKEN` to Lego. * Repeat the previous step for each host you want to run Lego on. -* It is possible to use the same api token for both variables if it is given `Zone:Read` and `DNS:Edit` permission for the zone. This "paranoid" setup is mainly interesting for users who manage many zones/domains with a single Cloudflare account. It follows the principle of least privilege and limits the possible damage, should one of the hosts become compromised. @@ -69,11 +68,10 @@ It follows the principle of least privilege and limits the possible damage, shou CLOUDFLARE_DNS_API_TOKEN = "Alias to CF_DNS_API_TOKEN" CLOUDFLARE_ZONE_API_TOKEN = "Alias to CF_ZONE_API_TOKEN" [Configuration.Additional] - CLOUDFLARE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - CLOUDFLARE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - CLOUDFLARE_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - CLOUDFLARE_HTTP_TIMEOUT = "API request timeout in seconds (Default: )" - CLOUDFLARE_BASE_URL = "API base URL (Default: https://api.cloudflare.com/client/v4)" + CLOUDFLARE_POLLING_INTERVAL = "Time between DNS propagation check" + CLOUDFLARE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + CLOUDFLARE_TTL = "The TTL of the TXT record used for the DNS challenge" + CLOUDFLARE_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://api.cloudflare.com/" diff --git a/providers/dns/cloudflare/cloudflare_test.go b/providers/dns/cloudflare/cloudflare_test.go index 8de9dd848..0aed51254 100644 --- a/providers/dns/cloudflare/cloudflare_test.go +++ b/providers/dns/cloudflare/cloudflare_test.go @@ -1,29 +1,20 @@ package cloudflare import ( - "net/http/httptest" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -const envDomain = envNamespace + "DOMAIN" - var envTest = tester.NewEnvTest( - EnvEmail, - EnvAPIKey, - EnvDNSAPIToken, - EnvZoneAPIToken, - EnvBaseURL, - altEnvEmail, - altEnvName(EnvAPIKey), - altEnvName(EnvDNSAPIToken), - altEnvName(EnvZoneAPIToken)). - WithDomain(envDomain) + "CLOUDFLARE_EMAIL", + "CLOUDFLARE_API_KEY", + "CLOUDFLARE_DNS_API_TOKEN", + "CLOUDFLARE_ZONE_API_TOKEN"). + WithDomain("CLOUDFLARE_DOMAIN") func TestNewDNSProvider(t *testing.T) { testCases := []struct { @@ -34,45 +25,45 @@ func TestNewDNSProvider(t *testing.T) { { desc: "success email, API key", envVars: map[string]string{ - EnvEmail: "test@example.com", - EnvAPIKey: "123", + "CLOUDFLARE_EMAIL": "test@example.com", + "CLOUDFLARE_API_KEY": "123", }, }, { desc: "success API token", envVars: map[string]string{ - EnvDNSAPIToken: "012345abcdef", + "CLOUDFLARE_DNS_API_TOKEN": "012345abcdef", }, }, { desc: "success separate API tokens", envVars: map[string]string{ - EnvDNSAPIToken: "012345abcdef", - EnvZoneAPIToken: "abcdef012345", + "CLOUDFLARE_DNS_API_TOKEN": "012345abcdef", + "CLOUDFLARE_ZONE_API_TOKEN": "abcdef012345", }, }, { desc: "missing credentials", envVars: map[string]string{ - EnvEmail: "", - EnvAPIKey: "", - EnvDNSAPIToken: "", + "CLOUDFLARE_EMAIL": "", + "CLOUDFLARE_API_KEY": "", + "CLOUDFLARE_DNS_API_TOKEN": "", }, expected: "cloudflare: some credentials information are missing: CLOUDFLARE_EMAIL,CLOUDFLARE_API_KEY or some credentials information are missing: CLOUDFLARE_DNS_API_TOKEN,CLOUDFLARE_ZONE_API_TOKEN", }, { desc: "missing email", envVars: map[string]string{ - EnvEmail: "", - EnvAPIKey: "key", + "CLOUDFLARE_EMAIL": "", + "CLOUDFLARE_API_KEY": "key", }, expected: "cloudflare: some credentials information are missing: CLOUDFLARE_EMAIL or some credentials information are missing: CLOUDFLARE_DNS_API_TOKEN,CLOUDFLARE_ZONE_API_TOKEN", }, { desc: "missing api key", envVars: map[string]string{ - EnvEmail: "awesome@possum.com", - EnvAPIKey: "", + "CLOUDFLARE_EMAIL": "awesome@possum.com", + "CLOUDFLARE_API_KEY": "", }, expected: "cloudflare: some credentials information are missing: CLOUDFLARE_API_KEY or some credentials information are missing: CLOUDFLARE_DNS_API_TOKEN,CLOUDFLARE_ZONE_API_TOKEN", }, @@ -81,7 +72,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -120,7 +110,7 @@ func TestNewDNSProviderWithToken(t *testing.T) { { desc: "same client when zone token is missing", envVars: map[string]string{ - EnvDNSAPIToken: "123", + "CLOUDFLARE_DNS_API_TOKEN": "123", }, expected: expected{ dnsToken: "123", @@ -131,8 +121,8 @@ func TestNewDNSProviderWithToken(t *testing.T) { { desc: "same client when zone token equals dns token", envVars: map[string]string{ - EnvDNSAPIToken: "123", - EnvZoneAPIToken: "123", + "CLOUDFLARE_DNS_API_TOKEN": "123", + "CLOUDFLARE_ZONE_API_TOKEN": "123", }, expected: expected{ dnsToken: "123", @@ -143,7 +133,7 @@ func TestNewDNSProviderWithToken(t *testing.T) { { desc: "failure when only zone api given", envVars: map[string]string{ - EnvZoneAPIToken: "123", + "CLOUDFLARE_ZONE_API_TOKEN": "123", }, expected: expected{ error: "cloudflare: some credentials information are missing: CLOUDFLARE_EMAIL,CLOUDFLARE_API_KEY or some credentials information are missing: CLOUDFLARE_DNS_API_TOKEN", @@ -152,8 +142,8 @@ func TestNewDNSProviderWithToken(t *testing.T) { { desc: "different clients when zone and dns token differ", envVars: map[string]string{ - EnvDNSAPIToken: "123", - EnvZoneAPIToken: "abc", + "CLOUDFLARE_DNS_API_TOKEN": "123", + "CLOUDFLARE_ZONE_API_TOKEN": "abc", }, expected: expected{ dnsToken: "123", @@ -164,10 +154,10 @@ func TestNewDNSProviderWithToken(t *testing.T) { { desc: "aliases work as expected", // CLOUDFLARE_* takes precedence over CF_* envVars: map[string]string{ - EnvDNSAPIToken: "123", - altEnvName(EnvDNSAPIToken): "456", - EnvZoneAPIToken: "abc", - altEnvName(EnvZoneAPIToken): "def", + "CLOUDFLARE_DNS_API_TOKEN": "123", + "CF_DNS_API_TOKEN": "456", + "CLOUDFLARE_ZONE_API_TOKEN": "abc", + "CF_ZONE_API_TOKEN": "def", }, expected: expected{ dnsToken: "123", @@ -178,18 +168,15 @@ func TestNewDNSProviderWithToken(t *testing.T) { } defer envTest.RestoreEnv() - localEnvTest := tester.NewEnvTest( - EnvDNSAPIToken, altEnvName(EnvDNSAPIToken), - EnvZoneAPIToken, altEnvName(EnvZoneAPIToken), - ).WithDomain(envDomain) - + "CLOUDFLARE_DNS_API_TOKEN", "CF_DNS_API_TOKEN", + "CLOUDFLARE_ZONE_API_TOKEN", "CF_ZONE_API_TOKEN", + ).WithDomain("CLOUDFLARE_DOMAIN") envTest.ClearEnv() for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer localEnvTest.RestoreEnv() - localEnvTest.ClearEnv() localEnvTest.Apply(test.envVars) @@ -204,7 +191,6 @@ func TestNewDNSProviderWithToken(t *testing.T) { require.NotNil(t, p) assert.Equal(t, test.expected.dnsToken, p.config.AuthToken) assert.Equal(t, test.expected.zoneToken, p.config.ZoneToken) - if test.expected.sameClient { assert.Equal(t, p.client.clientRead, p.client.clientEdit) } else { @@ -239,17 +225,22 @@ func TestNewDNSProviderConfig(t *testing.T) { }, { desc: "missing credentials", - expected: "cloudflare: invalid credentials: authEmail, authKey or authToken must be set", + expected: "cloudflare: invalid credentials: key & email must not be empty", }, { desc: "missing email", authKey: "123", - expected: "cloudflare: invalid credentials: authEmail and authKey must be set together", + expected: "cloudflare: invalid credentials: key & email must not be empty", }, { desc: "missing api key", authEmail: "test@example.com", - expected: "cloudflare: invalid credentials: authEmail and authKey must be set together", + expected: "cloudflare: invalid credentials: key & email must not be empty", + }, + { + desc: "missing api token, fallback to api key/email", + authToken: "", + expected: "cloudflare: invalid credentials: key & email must not be empty", }, } @@ -280,7 +271,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -294,7 +284,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -303,64 +292,3 @@ func TestLiveCleanUp(t *testing.T) { err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.AuthEmail = "foo@example.com" - config.AuthKey = "secret" - config.BaseURL = server.URL - config.HTTPClient = server.Client() - - return NewDNSProviderConfig(config) - }, - servermock.CheckHeader(). - WithRegexp("User-Agent", `goacme-lego/[0-9.]+ \(.+\)`). - With("X-Auth-Email", "foo@example.com"). - With("X-Auth-Key", "secret"), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - // https://developers.cloudflare.com/api/resources/zones/methods/list/ - Route("GET /zones", - servermock.ResponseFromInternal("zones.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "example.com"). - With("per_page", "50")). - // https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/create/ - Route("POST /zones/023e105f4ecef8ad9ca31a8372d0c353/dns_records", - servermock.ResponseFromInternal("create_record.json"), - servermock.CheckHeader(). - WithContentType("application/json"), - servermock.CheckRequestJSONBodyFromInternal("create_record-request.json")). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - // https://developers.cloudflare.com/api/resources/zones/methods/list/ - Route("GET /zones", - servermock.ResponseFromInternal("zones.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "example.com"). - With("per_page", "50")). - // https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/delete/ - Route("DELETE /zones/023e105f4ecef8ad9ca31a8372d0c353/dns_records/xxx", - servermock.ResponseFromInternal("delete_record.json")). - Build(t) - - token := "abc" - - provider.recordIDsMu.Lock() - provider.recordIDs["abc"] = "xxx" - provider.recordIDsMu.Unlock() - - err := provider.CleanUp("example.com", token, "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/cloudflare/internal/client.go b/providers/dns/cloudflare/internal/client.go deleted file mode 100644 index b63612ce2..000000000 --- a/providers/dns/cloudflare/internal/client.go +++ /dev/null @@ -1,202 +0,0 @@ -/* -Package internal Cloudflare API client. - -The official client is huge and still growing. -- https://github.com/cloudflare/cloudflare-go/issues/4171 -*/ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" -) - -const defaultBaseURL = "https://api.cloudflare.com/client/v4" - -// Client the Cloudflare API client. -type Client struct { - authEmail string - authKey string - authToken string - - baseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(opts ...Option) (*Client, error) { - baseURL, _ := url.Parse(defaultBaseURL) - - client := &Client{ - baseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - } - - for _, opt := range opts { - err := opt(client) - if err != nil { - return nil, err - } - } - - if client.authToken != "" { - return client, nil - } - - if client.authEmail == "" && client.authKey == "" { - return nil, errors.New("invalid credentials: authEmail, authKey or authToken must be set") - } - - if client.authEmail == "" || client.authKey == "" { - return nil, errors.New("invalid credentials: authEmail and authKey must be set together") - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return client, nil -} - -// CreateDNSRecord creates a new DNS record for a zone. -// https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/create/ -func (c *Client) CreateDNSRecord(ctx context.Context, zoneID string, record Record) (*Record, error) { - endpoint := c.baseURL.JoinPath("zones", zoneID, "dns_records") - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) - if err != nil { - return nil, err - } - - var result APIResponse[Record] - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return &result.Result, nil -} - -// DeleteDNSRecord deletes DNS record. -// https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/delete/ -func (c *Client) DeleteDNSRecord(ctx context.Context, zoneID, recordID string) error { - endpoint := c.baseURL.JoinPath("zones", zoneID, "dns_records", recordID) - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return err - } - - return c.do(req, nil) -} - -// ZonesByName returns a list of zones matching the given name. -// https://developers.cloudflare.com/api/resources/zones/methods/list/ -func (c *Client) ZonesByName(ctx context.Context, name string) ([]Zone, error) { - endpoint := c.baseURL.JoinPath("zones") - - query := endpoint.Query() - query.Set("name", name) - query.Set("per_page", "50") - endpoint.RawQuery = query.Encode() - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var result APIResponse[[]Zone] - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return result.Result, nil -} - -func (c *Client) do(req *http.Request, result any) error { - // https://developers.cloudflare.com/fundamentals/api/how-to/make-api-calls/ - if c.authToken != "" { - req.Header.Set("Authorization", "Bearer "+c.authToken) - } else { - req.Header.Set("X-Auth-Email", c.authEmail) - req.Header.Set("X-Auth-Key", c.authKey) - } - - useragent.SetHeader(req.Header) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var response APIResponse[any] - - err := json.Unmarshal(raw, &response) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return fmt.Errorf("[status code %d] %w", resp.StatusCode, response.Errors) -} diff --git a/providers/dns/cloudflare/internal/client_test.go b/providers/dns/cloudflare/internal/client_test.go deleted file mode 100644 index 9d286016f..000000000 --- a/providers/dns/cloudflare/internal/client_test.go +++ /dev/null @@ -1,176 +0,0 @@ -package internal - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient( - WithAuthKey("foo@example.com", "secret"), - WithHTTPClient(server.Client()), - WithBaseURL(server.URL), - ) - if err != nil { - return nil, err - } - - return client, nil - }, - servermock.CheckHeader(). - WithRegexp("User-Agent", `goacme-lego/[0-9.]+ \(.+\)`). - WithAccept("application/json"). - With("X-Auth-Email", "foo@example.com"). - With("X-Auth-Key", "secret"), - ) -} - -func TestClient_CreateDNSRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /zones/023e105f4ecef8ad9ca31a8372d0c353/dns_records", - servermock.ResponseFromFixture("create_record.json"), - servermock.CheckHeader(). - WithContentType("application/json"), - servermock.CheckRequestJSONBodyFromFixture("create_record-request.json")). - Build(t) - - record := Record{ - Name: "_acme-challenge.example.com", - TTL: 120, - Type: "TXT", - Content: `"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"`, - } - - newRecord, err := client.CreateDNSRecord(t.Context(), "023e105f4ecef8ad9ca31a8372d0c353", record) - require.NoError(t, err) - - expected := &Record{ - ID: "023e105f4ecef8ad9ca31a8372d0c353", - Name: "example.com", - TTL: 3600, - Type: "A", - Comment: "Domain verification record", - Content: "198.51.100.4", - } - - assert.Equal(t, expected, newRecord) -} - -func TestClient_CreateDNSRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /zones/023e105f4ecef8ad9ca31a8372d0c353/dns_records", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) - - record := Record{ - Name: "_acme-challenge.example.com", - TTL: 120, - Type: "TXT", - Content: `"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"`, - } - - _, err := client.CreateDNSRecord(t.Context(), "023e105f4ecef8ad9ca31a8372d0c353", record) - require.EqualError(t, err, "[status code 400] 6003: Invalid request headers; 6103: Invalid format for X-Auth-Key header") -} - -func TestClient_DeleteDNSRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /zones/023e105f4ecef8ad9ca31a8372d0c353/dns_records/xxx", - servermock.ResponseFromFixture("delete_record.json")). - Build(t) - - err := client.DeleteDNSRecord(context.Background(), "023e105f4ecef8ad9ca31a8372d0c353", "xxx") - require.NoError(t, err) -} - -func TestClient_DeleteDNSRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /zones/023e105f4ecef8ad9ca31a8372d0c353/dns_records/xxx", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) - - err := client.DeleteDNSRecord(context.Background(), "023e105f4ecef8ad9ca31a8372d0c353", "xxx") - require.EqualError(t, err, "[status code 400] 6003: Invalid request headers; 6103: Invalid format for X-Auth-Key header") -} - -func TestClient_ZonesByName(t *testing.T) { - client := mockBuilder(). - Route("GET /zones", - servermock.ResponseFromFixture("zones.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "example.com"). - With("per_page", "50")). - Build(t) - - zones, err := client.ZonesByName(context.Background(), "example.com") - require.NoError(t, err) - - expected := []Zone{ - { - ID: "023e105f4ecef8ad9ca31a8372d0c353", - Account: Account{ID: "023e105f4ecef8ad9ca31a8372d0c353", Name: "Example Account Name"}, - Meta: Meta{ - CdnOnly: true, - CustomCertificateQuota: 1, - DNSOnly: true, - FoundationDNS: true, - PageRuleQuota: 100, - PhishingDetected: false, - Step: 2, - }, - Name: "example.com", - Owner: Owner{ - ID: "023e105f4ecef8ad9ca31a8372d0c353", - Name: "Example Org", - Type: "organization", - }, - Plan: Plan{ - ID: "023e105f4ecef8ad9ca31a8372d0c353", - CanSubscribe: false, - Currency: "USD", - ExternallyManaged: false, - Frequency: "monthly", - IsSubscribed: false, - LegacyDiscount: false, - LegacyID: "free", - Price: 10, - Name: "Example Org", - }, - CnameSuffix: "cdn.cloudflare.com", - Paused: true, - Permissions: []string{"#worker:read"}, - Tenant: Tenant{ - ID: "023e105f4ecef8ad9ca31a8372d0c353", - Name: "Example Account Name", - }, - TenantUnit: TenantUnit{ - ID: "023e105f4ecef8ad9ca31a8372d0c353", - }, - Type: "full", - VanityNameServers: []string{"ns1.example.com", "ns2.example.com"}, - }, - } - - assert.Equal(t, expected, zones) -} - -func TestClient_ZonesByName_error(t *testing.T) { - client := mockBuilder(). - Route("GET /zones", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) - - _, err := client.ZonesByName(context.Background(), "example.com") - require.EqualError(t, err, "[status code 400] 6003: Invalid request headers; 6103: Invalid format for X-Auth-Key header") -} diff --git a/providers/dns/cloudflare/internal/fixtures/create_record-request.json b/providers/dns/cloudflare/internal/fixtures/create_record-request.json deleted file mode 100644 index 1b8604dc9..000000000 --- a/providers/dns/cloudflare/internal/fixtures/create_record-request.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "type": "TXT", - "name": "_acme-challenge.example.com", - "content": "\"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"", - "ttl": 120 -} diff --git a/providers/dns/cloudflare/internal/fixtures/create_record.json b/providers/dns/cloudflare/internal/fixtures/create_record.json deleted file mode 100644 index 7e08e993b..000000000 --- a/providers/dns/cloudflare/internal/fixtures/create_record.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "errors": [ - { - "code": 1000, - "message": "message", - "documentation_url": "documentation_url", - "source": { - "pointer": "pointer" - } - } - ], - "messages": [ - { - "code": 1000, - "message": "message", - "documentation_url": "documentation_url", - "source": { - "pointer": "pointer" - } - } - ], - "success": true, - "result": { - "name": "example.com", - "ttl": 3600, - "type": "A", - "comment": "Domain verification record", - "content": "198.51.100.4", - "proxied": true, - "settings": { - "ipv4_only": true, - "ipv6_only": true - }, - "tags": [ - "owner:dns-team" - ], - "id": "023e105f4ecef8ad9ca31a8372d0c353", - "proxiable": true - } -} diff --git a/providers/dns/cloudflare/internal/fixtures/delete_record.json b/providers/dns/cloudflare/internal/fixtures/delete_record.json deleted file mode 100644 index 038ac7b23..000000000 --- a/providers/dns/cloudflare/internal/fixtures/delete_record.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "result": { - "id": "023e105f4ecef8ad9ca31a8372d0c353" - } -} diff --git a/providers/dns/cloudflare/internal/fixtures/error.json b/providers/dns/cloudflare/internal/fixtures/error.json deleted file mode 100644 index 1b2360cc4..000000000 --- a/providers/dns/cloudflare/internal/fixtures/error.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "success": false, - "errors": [ - { - "code": 6003, - "message": "Invalid request headers", - "error_chain": [ - { - "code": 6103, - "message": "Invalid format for X-Auth-Key header" - } - ] - } - ], - "messages": [], - "result": null -} diff --git a/providers/dns/cloudflare/internal/fixtures/zones.json b/providers/dns/cloudflare/internal/fixtures/zones.json deleted file mode 100644 index 1dd94c4e3..000000000 --- a/providers/dns/cloudflare/internal/fixtures/zones.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "errors": [ - { - "code": 1000, - "message": "message", - "documentation_url": "documentation_url", - "source": { - "pointer": "pointer" - } - } - ], - "messages": [ - { - "code": 1000, - "message": "message", - "documentation_url": "documentation_url", - "source": { - "pointer": "pointer" - } - } - ], - "success": true, - "result": [ - { - "id": "023e105f4ecef8ad9ca31a8372d0c353", - "account": { - "id": "023e105f4ecef8ad9ca31a8372d0c353", - "name": "Example Account Name" - }, - "meta": { - "cdn_only": true, - "custom_certificate_quota": 1, - "dns_only": true, - "foundation_dns": true, - "page_rule_quota": 100, - "phishing_detected": false, - "step": 2 - }, - "name": "example.com", - "owner": { - "id": "023e105f4ecef8ad9ca31a8372d0c353", - "name": "Example Org", - "type": "organization" - }, - "plan": { - "id": "023e105f4ecef8ad9ca31a8372d0c353", - "can_subscribe": false, - "currency": "USD", - "externally_managed": false, - "frequency": "monthly", - "is_subscribed": false, - "legacy_discount": false, - "legacy_id": "free", - "price": 10, - "name": "Example Org" - }, - "cname_suffix": "cdn.cloudflare.com", - "paused": true, - "permissions": [ - "#worker:read" - ], - "tenant": { - "id": "023e105f4ecef8ad9ca31a8372d0c353", - "name": "Example Account Name" - }, - "tenant_unit": { - "id": "023e105f4ecef8ad9ca31a8372d0c353" - }, - "type": "full", - "vanity_name_servers": [ - "ns1.example.com", - "ns2.example.com" - ] - } - ], - "result_info": { - "count": 1, - "page": 1, - "per_page": 20, - "total_count": 1, - "total_pages": 1 - } -} diff --git a/providers/dns/cloudflare/internal/options.go b/providers/dns/cloudflare/internal/options.go deleted file mode 100644 index aa551a422..000000000 --- a/providers/dns/cloudflare/internal/options.go +++ /dev/null @@ -1,52 +0,0 @@ -package internal - -import ( - "net/http" - "net/url" -) - -type Option func(c *Client) error - -func WithAuthKey(authEmail, authKey string) Option { - return func(c *Client) error { - c.authEmail = authEmail - c.authKey = authKey - - return nil - } -} - -func WithAuthToken(authToken string) Option { - return func(c *Client) error { - c.authToken = authToken - - return nil - } -} - -func WithBaseURL(baseURL string) Option { - return func(c *Client) error { - if baseURL == "" { - return nil - } - - bu, err := url.Parse(baseURL) - if err != nil { - return err - } - - c.baseURL = bu - - return nil - } -} - -func WithHTTPClient(client *http.Client) Option { - return func(c *Client) error { - if client != nil { - c.HTTPClient = client - } - - return nil - } -} diff --git a/providers/dns/cloudflare/internal/types.go b/providers/dns/cloudflare/internal/types.go deleted file mode 100644 index 50a7bbbf9..000000000 --- a/providers/dns/cloudflare/internal/types.go +++ /dev/null @@ -1,123 +0,0 @@ -package internal - -import ( - "fmt" - "strings" -) - -type Record struct { - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - TTL int `json:"ttl,omitempty"` - Type string `json:"type,omitempty"` - Comment string `json:"comment,omitempty"` - Content string `json:"content,omitempty"` -} - -type APIResponse[T any] struct { - Errors Errors `json:"errors,omitempty"` - Messages []Message `json:"messages,omitempty"` - Success bool `json:"success,omitempty"` - Result T `json:"result,omitempty"` - ResultInfo *ResultInfo `json:"result_info,omitempty"` -} - -type Message struct { - Code int `json:"code"` - Message string `json:"message"` - DocumentationURL string `json:"documentation_url"` - Source *Source `json:"source"` - ErrorChain []ErrorChain `json:"error_chain"` -} - -type Source struct { - Pointer string `json:"pointer"` -} - -type ErrorChain struct { - Code int `json:"code"` - Message string `json:"message"` -} - -type Errors []Message - -func (e Errors) Error() string { - msg := new(strings.Builder) - - for _, item := range e { - _, _ = fmt.Fprintf(msg, "%d: %s", item.Code, item.Message) - - for _, link := range item.ErrorChain { - _, _ = fmt.Fprintf(msg, "; %d: %s", link.Code, link.Message) - } - } - - return msg.String() -} - -type ResultInfo struct { - Count int `json:"count"` - Page int `json:"page"` - PerPage int `json:"per_page"` - TotalCount int `json:"total_count"` - TotalPages int `json:"total_pages"` -} - -type Zone struct { - ID string `json:"id"` - Account Account `json:"account"` - Meta Meta `json:"meta"` - Name string `json:"name"` - Owner Owner `json:"owner"` - Plan Plan `json:"plan"` - CnameSuffix string `json:"cname_suffix"` - Paused bool `json:"paused"` - Permissions []string `json:"permissions"` - Tenant Tenant `json:"tenant"` - TenantUnit TenantUnit `json:"tenant_unit"` - Type string `json:"type"` - VanityNameServers []string `json:"vanity_name_servers"` -} - -type Account struct { - ID string `json:"id"` - Name string `json:"name"` -} - -type Meta struct { - CdnOnly bool `json:"cdn_only"` - CustomCertificateQuota int `json:"custom_certificate_quota"` - DNSOnly bool `json:"dns_only"` - FoundationDNS bool `json:"foundation_dns"` - PageRuleQuota int `json:"page_rule_quota"` - PhishingDetected bool `json:"phishing_detected"` - Step int `json:"step"` -} - -type Owner struct { - ID string `json:"id"` - Name string `json:"name"` - Type string `json:"type"` -} - -type Plan struct { - ID string `json:"id"` - CanSubscribe bool `json:"can_subscribe"` - Currency string `json:"currency"` - ExternallyManaged bool `json:"externally_managed"` - Frequency string `json:"frequency"` - IsSubscribed bool `json:"is_subscribed"` - LegacyDiscount bool `json:"legacy_discount"` - LegacyID string `json:"legacy_id"` - Price int `json:"price"` - Name string `json:"name"` -} - -type Tenant struct { - ID string `json:"id"` - Name string `json:"name"` -} - -type TenantUnit struct { - ID string `json:"id"` -} diff --git a/providers/dns/cloudflare/wrapper.go b/providers/dns/cloudflare/wrapper.go index 286c20ecd..a93feeded 100644 --- a/providers/dns/cloudflare/wrapper.go +++ b/providers/dns/cloudflare/wrapper.go @@ -2,16 +2,15 @@ package cloudflare import ( "context" - "errors" "sync" + "github.com/cloudflare/cloudflare-go" "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/providers/dns/cloudflare/internal" ) type metaClient struct { - clientEdit *internal.Client // needs Zone/DNS/Edit permissions - clientRead *internal.Client // needs Zone/Zone/Read permissions + clientEdit *cloudflare.API // needs Zone/DNS/Edit permissions + clientRead *cloudflare.API // needs Zone/Zone/Read permissions zones map[string]string // caches calls to ZoneIDByName, see lookupZoneID() zonesMu *sync.RWMutex @@ -20,10 +19,7 @@ type metaClient struct { func newClient(config *Config) (*metaClient, error) { // with AuthKey/AuthEmail we can access all available APIs if config.AuthToken == "" { - client, err := internal.NewClient( - internal.WithBaseURL(config.BaseURL), - internal.WithHTTPClient(config.HTTPClient), - internal.WithAuthKey(config.AuthEmail, config.AuthKey)) + client, err := cloudflare.New(config.AuthKey, config.AuthEmail, cloudflare.HTTPClient(config.HTTPClient)) if err != nil { return nil, err } @@ -36,10 +32,7 @@ func newClient(config *Config) (*metaClient, error) { }, nil } - dns, err := internal.NewClient( - internal.WithBaseURL(config.BaseURL), - internal.WithHTTPClient(config.HTTPClient), - internal.WithAuthToken(config.AuthToken)) + dns, err := cloudflare.NewWithAPIToken(config.AuthToken, cloudflare.HTTPClient(config.HTTPClient)) if err != nil { return nil, err } @@ -53,10 +46,7 @@ func newClient(config *Config) (*metaClient, error) { }, nil } - zone, err := internal.NewClient( - internal.WithBaseURL(config.BaseURL), - internal.WithHTTPClient(config.HTTPClient), - internal.WithAuthToken(config.ZoneToken)) + zone, err := cloudflare.NewWithAPIToken(config.ZoneToken, cloudflare.HTTPClient(config.HTTPClient)) if err != nil { return nil, err } @@ -69,15 +59,19 @@ func newClient(config *Config) (*metaClient, error) { }, nil } -func (m *metaClient) CreateDNSRecord(ctx context.Context, zoneID string, rr internal.Record) (*internal.Record, error) { - return m.clientEdit.CreateDNSRecord(ctx, zoneID, rr) +func (m *metaClient) CreateDNSRecord(ctx context.Context, zoneID string, rr cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error) { + return m.clientEdit.CreateDNSRecord(ctx, cloudflare.ZoneIdentifier(zoneID), rr) +} + +func (m *metaClient) DNSRecords(ctx context.Context, zoneID string, rr cloudflare.ListDNSRecordsParams) ([]cloudflare.DNSRecord, *cloudflare.ResultInfo, error) { + return m.clientEdit.ListDNSRecords(ctx, cloudflare.ZoneIdentifier(zoneID), rr) } func (m *metaClient) DeleteDNSRecord(ctx context.Context, zoneID, recordID string) error { - return m.clientEdit.DeleteDNSRecord(ctx, zoneID, recordID) + return m.clientEdit.DeleteDNSRecord(ctx, cloudflare.ZoneIdentifier(zoneID), recordID) } -func (m *metaClient) ZoneIDByName(ctx context.Context, fdqn string) (string, error) { +func (m *metaClient) ZoneIDByName(fdqn string) (string, error) { m.zonesMu.RLock() id := m.zones[fdqn] m.zonesMu.RUnlock() @@ -86,12 +80,7 @@ func (m *metaClient) ZoneIDByName(ctx context.Context, fdqn string) (string, err return id, nil } - zones, err := m.clientRead.ZonesByName(ctx, dns01.UnFqdn(fdqn)) - if err != nil { - return "", err - } - - id, err = extractZoneID(zones) + id, err := m.clientRead.ZoneIDByName(dns01.UnFqdn(fdqn)) if err != nil { return "", err } @@ -99,17 +88,5 @@ func (m *metaClient) ZoneIDByName(ctx context.Context, fdqn string) (string, err m.zonesMu.Lock() m.zones[fdqn] = id m.zonesMu.Unlock() - return id, nil } - -func extractZoneID(res []internal.Zone) (string, error) { - switch len(res) { - case 0: - return "", errors.New("zone could not be found") - case 1: - return res[0].ID, nil - default: - return "", errors.New("ambiguous zone name; an account ID might help") - } -} diff --git a/providers/dns/cloudns/cloudns.go b/providers/dns/cloudns/cloudns.go index 916d73bde..554e54163 100644 --- a/providers/dns/cloudns/cloudns.go +++ b/providers/dns/cloudns/cloudns.go @@ -8,14 +8,11 @@ import ( "net/http" "time" - "github.com/cenkalti/backoff/v5" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/platform/wait" "github.com/go-acme/lego/v4/providers/dns/cloudns/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -32,8 +29,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { AuthID string @@ -68,7 +63,6 @@ type DNSProvider struct { // CLOUDNS_AUTH_ID and CLOUDNS_AUTH_PASSWORD. func NewDNSProvider() (*DNSProvider, error) { var subAuthID string - authID := env.GetOrFile(EnvAuthID) if authID == "" { subAuthID = env.GetOrFile(EnvSubAuthID) @@ -102,11 +96,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, fmt.Errorf("ClouDNS: %w", err) } - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + client.HTTPClient = config.HTTPClient return &DNSProvider{client: client, config: config}, nil } @@ -169,22 +159,14 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // waitNameservers At the time of writing 4 servers are found as authoritative, but 8 are reported during the sync. // If this is not done, the secondary verification done by Let's Encrypt server will fail quire a bit. func (d *DNSProvider) waitNameservers(ctx context.Context, domain string, zone *internal.Zone) error { - return wait.Retry(ctx, - func() error { - syncProgress, err := d.client.GetUpdateStatus(ctx, zone.Name) - if err != nil { - return fmt.Errorf("nameserver sync on %s: %w", domain, err) - } + return wait.For("Nameserver sync on "+domain, d.config.PropagationTimeout, d.config.PollingInterval, func() (bool, error) { + syncProgress, err := d.client.GetUpdateStatus(ctx, zone.Name) + if err != nil { + return false, err + } - log.Infof("[%s] Sync %d/%d complete", domain, syncProgress.Updated, syncProgress.Total) + log.Infof("[%s] Sync %d/%d complete", domain, syncProgress.Updated, syncProgress.Total) - if !syncProgress.Complete { - return fmt.Errorf("nameserver sync on %s not complete", domain) - } - - return nil - }, - backoff.WithBackOff(backoff.NewConstantBackOff(d.config.PollingInterval)), - backoff.WithMaxElapsedTime(d.config.PropagationTimeout), - ) + return syncProgress.Complete, nil + }) } diff --git a/providers/dns/cloudns/cloudns.toml b/providers/dns/cloudns/cloudns.toml index ad52ef5b1..517bff750 100644 --- a/providers/dns/cloudns/cloudns.toml +++ b/providers/dns/cloudns/cloudns.toml @@ -7,7 +7,7 @@ Since = "v2.3.0" Example = ''' CLOUDNS_AUTH_ID=xxxx \ CLOUDNS_AUTH_PASSWORD=yyyy \ -lego --dns cloudns -d '*.example.com' -d example.com run +lego --email you@example.com --dns cloudns --domains my.example.org run ''' [Configuration] @@ -16,10 +16,10 @@ lego --dns cloudns -d '*.example.com' -d example.com run CLOUDNS_AUTH_PASSWORD = "The password for API user ID" [Configuration.Additional] CLOUDNS_SUB_AUTH_ID = "The API sub user ID" - CLOUDNS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" - CLOUDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 180)" - CLOUDNS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" - CLOUDNS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + CLOUDNS_POLLING_INTERVAL = "Time between DNS propagation check" + CLOUDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + CLOUDNS_TTL = "The TTL of the TXT record used for the DNS challenge" + CLOUDNS_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.cloudns.net/wiki/article/42/" diff --git a/providers/dns/cloudns/cloudns_test.go b/providers/dns/cloudns/cloudns_test.go index 024bd93d8..ea4f25c95 100644 --- a/providers/dns/cloudns/cloudns_test.go +++ b/providers/dns/cloudns/cloudns_test.go @@ -79,7 +79,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -170,7 +169,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -184,7 +182,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/cloudns/internal/client.go b/providers/dns/cloudns/internal/client.go index 278b8de49..60d7e6bbe 100644 --- a/providers/dns/cloudns/internal/client.go +++ b/providers/dns/cloudns/internal/client.go @@ -171,7 +171,6 @@ func (c *Client) ListTxtRecords(ctx context.Context, zoneName, fqdn string) ([]T } var records []TXTRecord - for _, record := range raw { if record.Host == subDomain && record.Type == "TXT" { records = append(records, record) @@ -280,7 +279,6 @@ func (c *Client) GetUpdateStatus(ctx context.Context, zoneName string) (*SyncPro } updatedCount := 0 - for _, record := range records { if record.Updated { updatedCount++ diff --git a/providers/dns/cloudns/internal/client_test.go b/providers/dns/cloudns/internal/client_test.go index b9f6c5431..999bd1446 100644 --- a/providers/dns/cloudns/internal/client_test.go +++ b/providers/dns/cloudns/internal/client_test.go @@ -1,26 +1,44 @@ package internal import ( + "context" + "fmt" + "net/http" "net/http/httptest" "net/url" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func setupClient(subAuthID string) func(server *httptest.Server) (*Client, error) { - return func(server *httptest.Server) (*Client, error) { - client, err := NewClient("myAuthID", subAuthID, "myAuthPassword") - if err != nil { - return nil, err +func setupTest(t *testing.T, subAuthID string, handler http.HandlerFunc) *Client { + t.Helper() + + server := httptest.NewServer(handler) + t.Cleanup(server.Close) + + client, err := NewClient("myAuthID", subAuthID, "myAuthPassword") + require.NoError(t, err) + + client.BaseURL, _ = url.Parse(server.URL) + client.HTTPClient = server.Client() + + return client +} + +func handlerMock(method string, jsonData []byte) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, "Incorrect method used", http.StatusBadRequest) + return } - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil + _, err := rw.Write(jsonData) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } } } @@ -79,7 +97,7 @@ func TestClient_GetZone(t *testing.T) { desc string authFQDN string apiResponse string - expected expected + expected }{ { desc: "zone found", @@ -114,17 +132,9 @@ func TestClient_GetZone(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient("")). - Route("GET /get-zone-info.json", - servermock.RawStringResponse(test.apiResponse), - servermock.CheckQueryParameter().Strict(). - With("auth-id", "myAuthID"). - With("auth-password", "myAuthPassword"). - With("domain-name", "foo.com"), - ). - Build(t) + client := setupTest(t, "", handlerMock(http.MethodGet, []byte(test.apiResponse))) - zone, err := client.GetZone(t.Context(), test.authFQDN) + zone, err := client.GetZone(context.Background(), test.authFQDN) if test.expected.errorMsg != "" { require.EqualError(t, err, test.expected.errorMsg) @@ -147,7 +157,7 @@ func TestClient_FindTxtRecord(t *testing.T) { authFQDN string zoneName string apiResponse string - expected expected + expected }{ { desc: "record found", @@ -229,19 +239,9 @@ func TestClient_FindTxtRecord(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient("")). - Route("GET /records.json", - servermock.RawStringResponse(test.apiResponse), - servermock.CheckQueryParameter().Strict(). - With("auth-id", "myAuthID"). - With("auth-password", "myAuthPassword"). - With("type", "TXT"). - With("host", "_acme-challenge"). - With("domain-name", test.zoneName), - ). - Build(t) + client := setupTest(t, "", handlerMock(http.MethodGet, []byte(test.apiResponse))) - txtRecord, err := client.FindTxtRecord(t.Context(), test.zoneName, test.authFQDN) + txtRecord, err := client.FindTxtRecord(context.Background(), test.zoneName, test.authFQDN) if test.expected.errorMsg != "" { require.EqualError(t, err, test.expected.errorMsg) @@ -264,7 +264,7 @@ func TestClient_ListTxtRecord(t *testing.T) { authFQDN string zoneName string apiResponse string - expected expected + expected }{ { desc: "record found", @@ -348,19 +348,9 @@ func TestClient_ListTxtRecord(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient("")). - Route("GET /records.json", - servermock.RawStringResponse(test.apiResponse), - servermock.CheckQueryParameter().Strict(). - With("auth-id", "myAuthID"). - With("auth-password", "myAuthPassword"). - With("type", "TXT"). - With("host", "_acme-challenge"). - With("domain-name", test.zoneName), - ). - Build(t) + client := setupTest(t, "", handlerMock(http.MethodGet, []byte(test.apiResponse))) - txtRecords, err := client.ListTxtRecords(t.Context(), test.zoneName, test.authFQDN) + txtRecords, err := client.ListTxtRecords(context.Background(), test.zoneName, test.authFQDN) if test.expected.errorMsg != "" { require.EqualError(t, err, test.expected.errorMsg) @@ -374,7 +364,7 @@ func TestClient_ListTxtRecord(t *testing.T) { func TestClient_AddTxtRecord(t *testing.T) { type expected struct { - query url.Values + query string errorMsg string } @@ -387,7 +377,7 @@ func TestClient_AddTxtRecord(t *testing.T) { value string ttl int apiResponse string - expected expected + expected }{ { desc: "sub-zone", @@ -398,15 +388,7 @@ func TestClient_AddTxtRecord(t *testing.T) { ttl: 60, apiResponse: `{"status":"Success","statusDescription":"The record was added successfully."}`, expected: expected{ - query: url.Values{ - "auth-id": {"myAuthID"}, - "auth-password": {"myAuthPassword"}, - "domain-name": {"example.com"}, - "host": {"_acme-challenge.foo"}, - "record": {"txtTXTtxtTXTtxtTXTtxtTXT"}, - "record-type": {"TXT"}, - "ttl": {"60"}, - }, + query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=example.com&host=_acme-challenge.foo&record=txtTXTtxtTXTtxtTXTtxtTXT&record-type=TXT&ttl=60`, }, }, { @@ -418,15 +400,7 @@ func TestClient_AddTxtRecord(t *testing.T) { ttl: 60, apiResponse: `{"status":"Success","statusDescription":"The record was added successfully."}`, expected: expected{ - query: url.Values{ - "auth-id": {"myAuthID"}, - "auth-password": {"myAuthPassword"}, - "domain-name": {"example.com"}, - "host": {"_acme-challenge"}, - "record": {"TXTtxtTXTtxtTXTtxtTXTtxt"}, - "record-type": {"TXT"}, - "ttl": {"60"}, - }, + query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=example.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=60`, }, }, { @@ -438,15 +412,7 @@ func TestClient_AddTxtRecord(t *testing.T) { ttl: 60, apiResponse: `{"status":"Success","statusDescription":"The record was added successfully."}`, expected: expected{ - query: url.Values{ - "auth-password": {"myAuthPassword"}, - "domain-name": {"example.com"}, - "host": {"_acme-challenge"}, - "record": {"TXTtxtTXTtxtTXTtxtTXTtxt"}, - "record-type": {"TXT"}, - "sub-auth-id": {"mySubAuthID"}, - "ttl": {"60"}, - }, + query: `auth-password=myAuthPassword&domain-name=example.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&sub-auth-id=mySubAuthID&ttl=60`, }, }, { @@ -458,15 +424,7 @@ func TestClient_AddTxtRecord(t *testing.T) { ttl: 120, apiResponse: `{"status":"Failed","statusDescription":"Invalid TTL. Choose from the list of the values we support."}`, expected: expected{ - query: url.Values{ - "auth-id": {"myAuthID"}, - "auth-password": {"myAuthPassword"}, - "domain-name": {"example.com"}, - "host": {"_acme-challenge"}, - "record": {"TXTtxtTXTtxtTXTtxtTXTtxt"}, - "record-type": {"TXT"}, - "ttl": {"300"}, - }, + query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=example.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=300`, errorMsg: "failed to add TXT record: Failed Invalid TTL. Choose from the list of the values we support.", }, }, @@ -479,15 +437,7 @@ func TestClient_AddTxtRecord(t *testing.T) { ttl: 120, apiResponse: `[{}]`, expected: expected{ - query: url.Values{ - "auth-id": {"myAuthID"}, - "auth-password": {"myAuthPassword"}, - "domain-name": {"example.com"}, - "host": {"_acme-challenge"}, - "record": {"TXTtxtTXTtxtTXTtxtTXTtxt"}, - "record-type": {"TXT"}, - "ttl": {"300"}, - }, + query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=example.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=300`, errorMsg: "unable to unmarshal response: [status code: 200] body: [{}] error: json: cannot unmarshal array into Go value of type internal.apiResponse", }, }, @@ -495,15 +445,17 @@ func TestClient_AddTxtRecord(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient(test.subAuthID)). - Route("POST /add-record.json", - servermock.RawStringResponse(test.apiResponse), - servermock.CheckQueryParameter().Strict(). - WithValues(test.expected.query), - ). - Build(t) + client := setupTest(t, test.subAuthID, func(rw http.ResponseWriter, req *http.Request) { + if test.expected.query != req.URL.RawQuery { + msg := fmt.Sprintf("got: %s, want: %s", test.expected.query, req.URL.RawQuery) + http.Error(rw, msg, http.StatusBadRequest) + return + } - err := client.AddTxtRecord(t.Context(), test.zoneName, test.authFQDN, test.value, test.ttl) + handlerMock(http.MethodPost, []byte(test.apiResponse))(rw, req) + }) + + err := client.AddTxtRecord(context.Background(), test.zoneName, test.authFQDN, test.value, test.ttl) if test.expected.errorMsg != "" { require.EqualError(t, err, test.expected.errorMsg) @@ -516,7 +468,7 @@ func TestClient_AddTxtRecord(t *testing.T) { func TestClient_RemoveTxtRecord(t *testing.T) { type expected struct { - query url.Values + query string errorMsg string } @@ -525,7 +477,7 @@ func TestClient_RemoveTxtRecord(t *testing.T) { id int zoneName string apiResponse string - expected expected + expected }{ { desc: "record found", @@ -533,12 +485,7 @@ func TestClient_RemoveTxtRecord(t *testing.T) { zoneName: "foo.com", apiResponse: `{ "status": "Success", "statusDescription": "The record was deleted successfully." }`, expected: expected{ - query: url.Values{ - "auth-id": {"myAuthID"}, - "auth-password": {"myAuthPassword"}, - "domain-name": {"foo.com"}, - "record-id": {"5769228"}, - }, + query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=foo.com&record-id=5769228`, }, }, { @@ -547,12 +494,7 @@ func TestClient_RemoveTxtRecord(t *testing.T) { zoneName: "foo.com", apiResponse: `{ "status": "Failed", "statusDescription": "Invalid record-id param." }`, expected: expected{ - query: url.Values{ - "auth-id": {"myAuthID"}, - "auth-password": {"myAuthPassword"}, - "domain-name": {"foo.com"}, - "record-id": {"5769000"}, - }, + query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=foo.com&record-id=5769000`, errorMsg: "failed to remove TXT record: Failed Invalid record-id param.", }, }, @@ -562,12 +504,7 @@ func TestClient_RemoveTxtRecord(t *testing.T) { zoneName: "foo-plus.com", apiResponse: `[{}]`, expected: expected{ - query: url.Values{ - "auth-id": {"myAuthID"}, - "auth-password": {"myAuthPassword"}, - "domain-name": {"foo-plus.com"}, - "record-id": {"44"}, - }, + query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=foo-plus.com&record-id=44`, errorMsg: "unable to unmarshal response: [status code: 200] body: [{}] error: json: cannot unmarshal array into Go value of type internal.apiResponse", }, }, @@ -575,15 +512,23 @@ func TestClient_RemoveTxtRecord(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient("")). - Route("POST /delete-record.json", - servermock.RawStringResponse(test.apiResponse), - servermock.CheckQueryParameter().Strict(). - WithValues(test.expected.query), - ). - Build(t) + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if test.expected.query != req.URL.RawQuery { + msg := fmt.Sprintf("got: %s, want: %s", test.expected.query, req.URL.RawQuery) + http.Error(rw, msg, http.StatusBadRequest) + return + } - err := client.RemoveTxtRecord(t.Context(), test.id, test.zoneName) + handlerMock(http.MethodPost, []byte(test.apiResponse))(rw, req) + })) + t.Cleanup(server.Close) + + client, err := NewClient("myAuthID", "", "myAuthPassword") + require.NoError(t, err) + + client.BaseURL, _ = url.Parse(server.URL) + + err = client.RemoveTxtRecord(context.Background(), test.id, test.zoneName) if test.expected.errorMsg != "" { require.EqualError(t, err, test.expected.errorMsg) @@ -605,7 +550,7 @@ func TestClient_GetUpdateStatus(t *testing.T) { authFQDN string zoneName string apiResponse string - expected expected + expected }{ { desc: "50% sync", @@ -645,17 +590,15 @@ func TestClient_GetUpdateStatus(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient("")). - Route("GET /update-status.json", - servermock.RawStringResponse(test.apiResponse), - servermock.CheckQueryParameter().Strict(). - With("auth-id", "myAuthID"). - With("auth-password", "myAuthPassword"). - With("domain-name", test.zoneName), - ). - Build(t) + server := httptest.NewServer(handlerMock(http.MethodGet, []byte(test.apiResponse))) + t.Cleanup(server.Close) - syncProgress, err := client.GetUpdateStatus(t.Context(), test.zoneName) + client, err := NewClient("myAuthID", "", "myAuthPassword") + require.NoError(t, err) + + client.BaseURL, _ = url.Parse(server.URL) + + syncProgress, err := client.GetUpdateStatus(context.Background(), test.zoneName) if test.expected.errorMsg != "" { require.EqualError(t, err, test.expected.errorMsg) diff --git a/providers/dns/cloudru/cloudru.go b/providers/dns/cloudru/cloudru.go index dd597952a..68ad21b26 100644 --- a/providers/dns/cloudru/cloudru.go +++ b/providers/dns/cloudru/cloudru.go @@ -10,11 +10,9 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/cloudru/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -32,8 +30,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { ServiceInstanceID string @@ -61,9 +57,8 @@ func NewDefaultConfig() *Config { } type DNSProvider struct { - config *Config - client *internal.Client - + config *Config + client *internal.Client records map[string]*internal.Record recordsMu sync.Mutex } @@ -101,8 +96,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, client: client, diff --git a/providers/dns/cloudru/cloudru.toml b/providers/dns/cloudru/cloudru.toml index b74098a72..19faf8d87 100644 --- a/providers/dns/cloudru/cloudru.toml +++ b/providers/dns/cloudru/cloudru.toml @@ -8,7 +8,7 @@ Example = ''' CLOUDRU_SERVICE_INSTANCE_ID=ppp \ CLOUDRU_KEY_ID=xxx \ CLOUDRU_SECRET=yyy \ -lego --dns cloudru -d '*.example.com' -d example.com run +lego --email you@example.com --dns cloudru --domains my.example.org run ''' [Configuration] @@ -17,11 +17,11 @@ lego --dns cloudru -d '*.example.com' -d example.com run CLOUDRU_KEY_ID = "Key ID (login)" CLOUDRU_SECRET = "Key Secret" [Configuration.Additional] - CLOUDRU_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 5)" - CLOUDRU_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 300)" - CLOUDRU_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - CLOUDRU_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - CLOUDRU_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 120)" + CLOUDRU_POLLING_INTERVAL = "Time between DNS propagation check" + CLOUDRU_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + CLOUDRU_TTL = "The TTL of the TXT record used for the DNS challenge" + CLOUDRU_HTTP_TIMEOUT = "API request timeout" + CLOUDRU_SEQUENCE_INTERVAL = "Time between sequential requests" [Links] API = "https://cloud.ru/ru/docs/clouddns/ug/topics/api-ref.html" diff --git a/providers/dns/cloudru/cloudru_test.go b/providers/dns/cloudru/cloudru_test.go index 3e506cb1c..88addde93 100644 --- a/providers/dns/cloudru/cloudru_test.go +++ b/providers/dns/cloudru/cloudru_test.go @@ -67,7 +67,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -154,7 +153,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -168,7 +166,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/cloudru/internal/client.go b/providers/dns/cloudru/internal/client.go index a00ae6ea8..cb62c5bca 100644 --- a/providers/dns/cloudru/internal/client.go +++ b/providers/dns/cloudru/internal/client.go @@ -61,7 +61,6 @@ func (c *Client) GetZones(ctx context.Context, parentID string) ([]Zone, error) } var zones APIResponse[Zone] - err = c.do(req, &zones) if err != nil { return nil, err @@ -79,7 +78,6 @@ func (c *Client) GetRecords(ctx context.Context, zoneID string) ([]Record, error } var records APIResponse[Record] - err = c.do(req, &records) if err != nil { return nil, err @@ -97,7 +95,6 @@ func (c *Client) CreateRecord(ctx context.Context, zoneID string, record Record) } var result Record - err = c.do(req, &result) if err != nil { return nil, err diff --git a/providers/dns/cloudru/internal/client_test.go b/providers/dns/cloudru/internal/client_test.go index 3b087d617..d96183d9f 100644 --- a/providers/dns/cloudru/internal/client_test.go +++ b/providers/dns/cloudru/internal/client_test.go @@ -1,42 +1,64 @@ package internal import ( + "fmt" + "io" + "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" "time" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("user", "secret") - client.HTTPClient = server.Client() - client.APIEndpoint, _ = url.Parse(server.URL) - client.token = &Token{ - AccessToken: "secret", - ExpiresIn: 60, - TokenType: "Bearer", - Deadline: time.Now().Add(1 * time.Minute), - } +func setupTest(t *testing.T, pattern string, handler http.HandlerFunc) *Client { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("Bearer xxx")) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc(pattern, handler) + + client := NewClient("user", "secret") + client.HTTPClient = server.Client() + client.APIEndpoint, _ = url.Parse(server.URL) + client.token = &Token{ + AccessToken: "secret", + ExpiresIn: 60, + TokenType: "Bearer", + Deadline: time.Now().Add(1 * time.Minute), + } + + return client +} + +func writeFixtureHandler(method, filename string) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, _ = io.Copy(rw, file) + } } func TestClient_GetZones(t *testing.T) { - client := mockBuilder(). - Route("GET /zones", - servermock.ResponseFromFixture("zones.json")). - Build(t) + client := setupTest(t, "/zones", writeFixtureHandler(http.MethodGet, "zones.json")) - ctx := mockContext(t) + ctx := mockContext() zones, err := client.GetZones(ctx, "xxx") require.NoError(t, err) @@ -56,12 +78,9 @@ func TestClient_GetZones(t *testing.T) { } func TestClient_GetRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /zones/zzz/records", - servermock.ResponseFromFixture("records.json")). - Build(t) + client := setupTest(t, "/zones/zzz/records", writeFixtureHandler(http.MethodGet, "records.json")) - ctx := mockContext(t) + ctx := mockContext() records, err := client.GetRecords(ctx, "zzz") require.NoError(t, err) @@ -103,13 +122,9 @@ func TestClient_GetRecords(t *testing.T) { } func TestClient_CreateRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /zones/zzz/records", - servermock.ResponseFromFixture("record.json"), - servermock.CheckRequestJSONBody(`{"name":"www.example.com.","type":"TXT","values":["text"],"ttl":"3600"}`)). - Build(t) + client := setupTest(t, "/zones/zzz/records", writeFixtureHandler(http.MethodPost, "record.json")) - ctx := mockContext(t) + ctx := mockContext() recordReq := Record{ Name: "www.example.com.", @@ -135,12 +150,9 @@ func TestClient_CreateRecord(t *testing.T) { } func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /zones/zzz/records/example.com/TXT", - servermock.ResponseFromFixture("record.json")). - Build(t) + client := setupTest(t, "/zones/zzz/records/example.com/TXT", writeFixtureHandler(http.MethodDelete, "record.json")) - ctx := mockContext(t) + ctx := mockContext() err := client.DeleteRecord(ctx, "zzz", "example.com", "TXT") require.NoError(t, err) diff --git a/providers/dns/cloudru/internal/identity.go b/providers/dns/cloudru/internal/identity.go index 3bb09f3fa..79df3c297 100644 --- a/providers/dns/cloudru/internal/identity.go +++ b/providers/dns/cloudru/internal/identity.go @@ -49,7 +49,6 @@ func (c *Client) obtainToken(ctx context.Context) (*Token, error) { } tok := Token{} - err = json.Unmarshal(raw, &tok) if err != nil { return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) @@ -89,7 +88,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) errResp := &authResponseError{} - err := json.Unmarshal(raw, errResp) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/cloudru/internal/identity_test.go b/providers/dns/cloudru/internal/identity_test.go index c1097c015..7329e7f55 100644 --- a/providers/dns/cloudru/internal/identity_test.go +++ b/providers/dns/cloudru/internal/identity_test.go @@ -2,51 +2,65 @@ package internal import ( "context" + "encoding/json" + "fmt" + "net/http" "net/http/httptest" "net/url" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockContext(t *testing.T) context.Context { - t.Helper() - - return context.WithValue(t.Context(), tokenKey, &Token{AccessToken: "xxx"}) +func mockContext() context.Context { + return context.WithValue(context.Background(), tokenKey, &Token{AccessToken: "xxx"}) } -func setupIdentityClient(server *httptest.Server) (*Client, error) { +func tokenHandler(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("invalid method, got %s want %s", req.Method, http.MethodPost), http.StatusMethodNotAllowed) + return + } + + err := req.ParseForm() + if err != nil { + http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + grantType := req.Form.Get("grant_type") + clientID := req.Form.Get("client_id") + clientSecret := req.Form.Get("client_secret") + + if clientID != "user" || clientSecret != "secret" || grantType != "access_key" { + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + _ = json.NewEncoder(rw).Encode(Token{ + AccessToken: "xxx", + TokenID: "yyy", + ExpiresIn: 666, + TokenType: "Bearer", + Scope: "openid profile email roles", + }) +} + +func TestClient_obtainToken(t *testing.T) { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc("/", tokenHandler) + client := NewClient("user", "secret") client.HTTPClient = server.Client() client.AuthEndpoint, _ = url.Parse(server.URL) - return client, nil -} - -func TestClient_obtainToken(t *testing.T) { - client := servermock.NewBuilder[*Client](setupIdentityClient, - servermock.CheckHeader(). - WithContentTypeFromURLEncoded(), - ). - Route("POST /", servermock.JSONEncode(Token{ - AccessToken: "xxx", - TokenID: "yyy", - ExpiresIn: 666, - TokenType: "Bearer", - Scope: "openid profile email roles", - }), - servermock.CheckForm().Strict(). - With("client_id", "user"). - With("client_secret", "secret"). - With("grant_type", "access_key"), - ). - Build(t) - assert.Nil(t, client.token) - tok, err := client.obtainToken(t.Context()) + tok, err := client.obtainToken(context.Background()) require.NoError(t, err) assert.NotNil(t, tok) @@ -55,27 +69,19 @@ func TestClient_obtainToken(t *testing.T) { } func TestClient_CreateAuthenticatedContext(t *testing.T) { - client := servermock.NewBuilder[*Client](setupIdentityClient, - servermock.CheckHeader(). - WithContentTypeFromURLEncoded(), - ). - Route("POST /", servermock.JSONEncode(Token{ - AccessToken: "xxx", - TokenID: "yyy", - ExpiresIn: 666, - TokenType: "Bearer", - Scope: "openid profile email roles", - }), - servermock.CheckForm().Strict(). - With("client_id", "user"). - With("client_secret", "secret"). - With("grant_type", "access_key"), - ). - Build(t) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc("/", tokenHandler) + + client := NewClient("user", "secret") + client.HTTPClient = server.Client() + client.AuthEndpoint, _ = url.Parse(server.URL) assert.Nil(t, client.token) - ctx, err := client.CreateAuthenticatedContext(t.Context()) + ctx, err := client.CreateAuthenticatedContext(context.Background()) require.NoError(t, err) tok := getToken(ctx) diff --git a/providers/dns/cloudru/internal/types.go b/providers/dns/cloudru/internal/types.go index 713fd459a..d233c73bc 100644 --- a/providers/dns/cloudru/internal/types.go +++ b/providers/dns/cloudru/internal/types.go @@ -38,9 +38,9 @@ type Zone struct { Valid bool `json:"valid,omitempty"` ValidationText string `json:"validationText,omitempty"` Delegated bool `json:"delegated,omitempty"` - LastCheck time.Time `json:"lastCheck,omitzero"` - CreatedAt time.Time `json:"created_at,omitzero"` - UpdatedAt time.Time `json:"updated_at,omitzero"` + LastCheck time.Time `json:"lastCheck,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` } type Record struct { diff --git a/providers/dns/cloudxns/cloudxns.go b/providers/dns/cloudxns/cloudxns.go index 25ff17573..6269b8da7 100644 --- a/providers/dns/cloudxns/cloudxns.go +++ b/providers/dns/cloudxns/cloudxns.go @@ -2,11 +2,15 @@ package cloudxns import ( + "context" "errors" + "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/go-acme/lego/v4/providers/dns/cloudxns/internal" ) // Environment variables names. @@ -34,34 +38,101 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { - return &Config{} + return &Config{ + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } } // DNSProvider implements the challenge.Provider interface. -type DNSProvider struct{} +type DNSProvider struct { + config *Config + client *internal.Client +} // NewDNSProvider returns a DNSProvider instance configured for CloudXNS. +// Credentials must be passed in the environment variables: +// CLOUDXNS_API_KEY and CLOUDXNS_SECRET_KEY. func NewDNSProvider() (*DNSProvider, error) { - return NewDNSProviderConfig(&Config{}) + values, err := env.Get(EnvAPIKey, EnvSecretKey) + if err != nil { + return nil, fmt.Errorf("cloudxns: %w", err) + } + + config := NewDefaultConfig() + config.APIKey = values[EnvAPIKey] + config.SecretKey = values[EnvSecretKey] + + return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for CloudXNS. -func NewDNSProviderConfig(_ *Config) (*DNSProvider, error) { - return nil, errors.New("cloudxns: provider has shut down") +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("cloudxns: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.APIKey, config.SecretKey) + if err != nil { + return nil, fmt.Errorf("cloudxns: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + return &DNSProvider{client: client, config: config}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. -func (d *DNSProvider) Present(_, _, _ string) error { +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + challengeInfo := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + info, err := d.client.GetDomainInformation(ctx, challengeInfo.EffectiveFQDN) + if err != nil { + return fmt.Errorf("cloudxns: %w", err) + } + + err = d.client.AddTxtRecord(ctx, info, challengeInfo.EffectiveFQDN, challengeInfo.Value, d.config.TTL) + if err != nil { + return fmt.Errorf("cloudxns: %w", err) + } + return nil } // CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(_, _, _ string) error { +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + challengeInfo := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + info, err := d.client.GetDomainInformation(ctx, challengeInfo.EffectiveFQDN) + if err != nil { + return fmt.Errorf("cloudxns: %w", err) + } + + record, err := d.client.FindTxtRecord(ctx, info.ID, challengeInfo.EffectiveFQDN) + if err != nil { + return fmt.Errorf("cloudxns: %w", err) + } + + err = d.client.RemoveTxtRecord(ctx, record.RecordID, info.ID) + if err != nil { + return fmt.Errorf("cloudxns: %w", err) + } + return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return dns01.DefaultPropagationTimeout, dns01.DefaultPollingInterval + return d.config.PropagationTimeout, d.config.PollingInterval } diff --git a/providers/dns/cloudxns/cloudxns.toml b/providers/dns/cloudxns/cloudxns.toml index 32eae8beb..4f5424b32 100644 --- a/providers/dns/cloudxns/cloudxns.toml +++ b/providers/dns/cloudxns/cloudxns.toml @@ -1,15 +1,13 @@ -Name = "CloudXNS (Deprecated)" -Description = ''' -The CloudXNS DNS provider has shut down. -''' -URL = "https://github.com/go-acme/lego/issues/2323" +Name = "CloudXNS" +Description = """""" +URL = "https://www.cloudxns.net/" Code = "cloudxns" Since = "v0.5.0" Example = ''' CLOUDXNS_API_KEY=xxxx \ CLOUDXNS_SECRET_KEY=yyyy \ -lego --dns cloudxns -d '*.example.com' -d example.com run +lego --email you@example.com --dns cloudxns --domains my.example.org run ''' [Configuration] @@ -17,7 +15,10 @@ lego --dns cloudxns -d '*.example.com' -d example.com run CLOUDXNS_API_KEY = "The API key" CLOUDXNS_SECRET_KEY = "The API secret key" [Configuration.Additional] - CLOUDXNS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: )" - CLOUDXNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: )" - CLOUDXNS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: )" - CLOUDXNS_HTTP_TIMEOUT = "API request timeout in seconds (Default: )" + CLOUDXNS_POLLING_INTERVAL = "Time between DNS propagation check" + CLOUDXNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + CLOUDXNS_TTL = "The TTL of the TXT record used for the DNS challenge" + CLOUDXNS_HTTP_TIMEOUT = "API request timeout" + +[Links] + API = "https://www.cloudxns.net/Public/Doc/CloudXNS_api2.0_doc_zh-cn.zip" diff --git a/providers/dns/aliesa/aliesa_test.go b/providers/dns/cloudxns/cloudxns_test.go similarity index 70% rename from providers/dns/aliesa/aliesa_test.go rename to providers/dns/cloudxns/cloudxns_test.go index 025529409..0b3271761 100644 --- a/providers/dns/aliesa/aliesa_test.go +++ b/providers/dns/cloudxns/cloudxns_test.go @@ -1,7 +1,8 @@ -package aliesa +package cloudxns import ( "testing" + "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" @@ -10,9 +11,8 @@ import ( const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( - EnvAccessKey, - EnvSecretKey, - EnvRAMRole). + EnvAPIKey, + EnvSecretKey). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { @@ -24,46 +24,39 @@ func TestNewDNSProvider(t *testing.T) { { desc: "success", envVars: map[string]string{ - EnvAccessKey: "123", + EnvAPIKey: "123", EnvSecretKey: "456", }, }, - { - desc: "success (RAM role)", - envVars: map[string]string{ - EnvRAMRole: "LegoInstanceRole", - }, - }, { desc: "missing credentials", envVars: map[string]string{ - EnvAccessKey: "", + EnvAPIKey: "", EnvSecretKey: "", }, - expected: "aliesa: some credentials information are missing: ALIESA_ACCESS_KEY,ALIESA_SECRET_KEY", + expected: "cloudxns: some credentials information are missing: CLOUDXNS_API_KEY,CLOUDXNS_SECRET_KEY", }, { - desc: "missing access key", + desc: "missing API key", envVars: map[string]string{ - EnvAccessKey: "", + EnvAPIKey: "", EnvSecretKey: "456", }, - expected: "aliesa: some credentials information are missing: ALIESA_ACCESS_KEY", + expected: "cloudxns: some credentials information are missing: CLOUDXNS_API_KEY", }, { desc: "missing secret key", envVars: map[string]string{ - EnvAccessKey: "123", + EnvAPIKey: "123", EnvSecretKey: "", }, - expected: "aliesa: some credentials information are missing: ALIESA_SECRET_KEY", + expected: "cloudxns: some credentials information are missing: CLOUDXNS_SECRET_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -85,7 +78,6 @@ func TestNewDNSProvider(t *testing.T) { func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string - ramRole string apiKey string secretKey string expected string @@ -95,23 +87,19 @@ func TestNewDNSProviderConfig(t *testing.T) { apiKey: "123", secretKey: "456", }, - { - desc: "success", - ramRole: "LegoInstanceRole", - }, { desc: "missing credentials", - expected: "aliesa: ram role or credentials missing", + expected: "cloudxns: credentials missing: apiKey", }, { desc: "missing api key", secretKey: "456", - expected: "aliesa: ram role or credentials missing", + expected: "cloudxns: credentials missing: apiKey", }, { desc: "missing secret key", apiKey: "123", - expected: "aliesa: ram role or credentials missing", + expected: "cloudxns: credentials missing: secretKey", }, } @@ -120,7 +108,6 @@ func TestNewDNSProviderConfig(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey config.SecretKey = test.secretKey - config.RAMRole = test.ramRole p, err := NewDNSProviderConfig(config) @@ -142,10 +129,24 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + time.Sleep(2 * time.Second) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/cloudxns/internal/client.go b/providers/dns/cloudxns/internal/client.go new file mode 100644 index 000000000..37f10fe87 --- /dev/null +++ b/providers/dns/cloudxns/internal/client.go @@ -0,0 +1,221 @@ +package internal + +import ( + "bytes" + "context" + "crypto/md5" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" +) + +const defaultBaseURL = "https://www.cloudxns.net/api2/" + +// Client CloudXNS client. +type Client struct { + apiKey string + secretKey string + + baseURL *url.URL + HTTPClient *http.Client +} + +// NewClient creates a CloudXNS client. +func NewClient(apiKey, secretKey string) (*Client, error) { + if apiKey == "" { + return nil, errors.New("credentials missing: apiKey") + } + + if secretKey == "" { + return nil, errors.New("credentials missing: secretKey") + } + + baseURL, _ := url.Parse(defaultBaseURL) + + return &Client{ + apiKey: apiKey, + secretKey: secretKey, + baseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +// GetDomainInformation Get domain name information for a FQDN. +func (c *Client) GetDomainInformation(ctx context.Context, fqdn string) (*Data, error) { + endpoint := c.baseURL.JoinPath("domain") + + req, err := c.newRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + authZone, err := dns01.FindZoneByFqdn(fqdn) + if err != nil { + return nil, fmt.Errorf("could not find zone: %w", err) + } + + var domains []Data + err = c.do(req, &domains) + if err != nil { + return nil, err + } + + for _, data := range domains { + if data.Domain == authZone { + return &data, nil + } + } + + return nil, fmt.Errorf("zone %s not found for domain %s", authZone, fqdn) +} + +// FindTxtRecord return the TXT record a zone ID and a FQDN. +func (c *Client) FindTxtRecord(ctx context.Context, zoneID, fqdn string) (*TXTRecord, error) { + endpoint := c.baseURL.JoinPath("record", zoneID) + + query := endpoint.Query() + query.Set("host_id", "0") + query.Set("offset", "0") + query.Set("row_num", "2000") + endpoint.RawQuery = query.Encode() + + req, err := c.newRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + var records []TXTRecord + err = c.do(req, &records) + if err != nil { + return nil, err + } + + for _, record := range records { + if record.Host == dns01.UnFqdn(fqdn) && record.Type == "TXT" { + return &record, nil + } + } + + return nil, fmt.Errorf("no existing record found for %q", fqdn) +} + +// AddTxtRecord add a TXT record. +func (c *Client) AddTxtRecord(ctx context.Context, info *Data, fqdn, value string, ttl int) error { + id, err := strconv.Atoi(info.ID) + if err != nil { + return fmt.Errorf("invalid zone ID: %w", err) + } + + endpoint := c.baseURL.JoinPath("record") + + subDomain, err := dns01.ExtractSubDomain(fqdn, info.Domain) + if err != nil { + return err + } + + record := TXTRecord{ + ID: id, + Host: subDomain, + Value: value, + Type: "TXT", + LineID: 1, + TTL: ttl, + } + + req, err := c.newRequest(ctx, http.MethodPost, endpoint, record) + if err != nil { + return err + } + + return c.do(req, nil) +} + +// RemoveTxtRecord remove a TXT record. +func (c *Client) RemoveTxtRecord(ctx context.Context, recordID, zoneID string) error { + endpoint := c.baseURL.JoinPath("record", recordID, zoneID) + + req, err := c.newRequest(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return err + } + + return c.do(req, nil) +} + +func (c *Client) do(req *http.Request, result any) error { + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + var response apiResponse + err = json.Unmarshal(raw, &response) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + if response.Code != 1 { + return fmt.Errorf("[status code %d] invalid code (%v) error: %s", resp.StatusCode, response.Code, response.Message) + } + + if result == nil { + return nil + } + + if len(response.Data) == 0 { + return nil + } + + err = json.Unmarshal(response.Data, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func (c *Client) newRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + requestDate := time.Now().Format(time.RFC1123Z) + + req.Header.Set("API-KEY", c.apiKey) + req.Header.Set("API-REQUEST-DATE", requestDate) + req.Header.Set("API-HMAC", c.hmac(endpoint.String(), requestDate, buf.String())) + req.Header.Set("API-FORMAT", "json") + + return req, nil +} + +func (c *Client) hmac(endpoint, date, body string) string { + sum := md5.Sum([]byte(c.apiKey + endpoint + body + date + c.secretKey)) + return hex.EncodeToString(sum[:]) +} diff --git a/providers/dns/cloudxns/internal/client_test.go b/providers/dns/cloudxns/internal/client_test.go new file mode 100644 index 000000000..ac4e36d6b --- /dev/null +++ b/providers/dns/cloudxns/internal/client_test.go @@ -0,0 +1,292 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupTest(t *testing.T, handler http.HandlerFunc) *Client { + t.Helper() + + server := httptest.NewServer(handler) + t.Cleanup(server.Close) + + client, _ := NewClient("myKey", "mySecret") + client.baseURL, _ = url.Parse(server.URL + "/") + client.HTTPClient = server.Client() + + return client +} + +func handlerMock(method string, response *apiResponse, data interface{}) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + content, err := json.Marshal(apiResponse{ + Code: 999, // random code only for the test + Message: fmt.Sprintf("invalid method: got %s want %s", req.Method, method), + }) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + http.Error(rw, string(content), http.StatusBadRequest) + return + } + + jsonData, err := json.Marshal(data) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + response.Data = jsonData + + content, err := json.Marshal(response) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + _, err = rw.Write(content) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + } +} + +func TestClient_GetDomainInformation(t *testing.T) { + type result struct { + domain *Data + error bool + } + + testCases := []struct { + desc string + fqdn string + response *apiResponse + data []Data + expected result + }{ + { + desc: "domain found", + fqdn: "_acme-challenge.example.org.", + response: &apiResponse{ + Code: 1, + }, + data: []Data{ + { + ID: "1", + Domain: "example.com.", + }, + { + ID: "2", + Domain: "example.org.", + }, + }, + expected: result{domain: &Data{ + ID: "2", + Domain: "example.org.", + }}, + }, + { + desc: "domains not found", + fqdn: "_acme-challenge.huu.com.", + response: &apiResponse{ + Code: 1, + }, + data: []Data{ + { + ID: "5", + Domain: "example.com.", + }, + { + ID: "6", + Domain: "example.org.", + }, + }, + expected: result{error: true}, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + client := setupTest(t, handlerMock(http.MethodGet, test.response, test.data)) + + domain, err := client.GetDomainInformation(context.Background(), test.fqdn) + + if test.expected.error { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, test.expected.domain, domain) + } + }) + } +} + +func TestClient_FindTxtRecord(t *testing.T) { + type result struct { + txtRecord *TXTRecord + error bool + } + + testCases := []struct { + desc string + fqdn string + zoneID string + txtRecords []TXTRecord + response *apiResponse + expected result + }{ + { + desc: "record found", + fqdn: "_acme-challenge.example.org.", + zoneID: "test-zone", + txtRecords: []TXTRecord{ + { + ID: 1, + RecordID: "Record-A", + Host: "_acme-challenge.example.org", + Value: "txtTXTtxtTXTtxtTXTtxtTXT", + Type: "TXT", + LineID: 6, + TTL: 30, + }, + { + ID: 2, + RecordID: "Record-B", + Host: "_acme-challenge.example.com", + Value: "TXTtxtTXTtxtTXTtxtTXTtxt", + Type: "TXT", + LineID: 6, + TTL: 30, + }, + }, + response: &apiResponse{ + Code: 1, + }, + expected: result{ + txtRecord: &TXTRecord{ + ID: 1, + RecordID: "Record-A", + Host: "_acme-challenge.example.org", + Value: "txtTXTtxtTXTtxtTXTtxtTXT", + Type: "TXT", + LineID: 6, + TTL: 30, + }, + }, + }, + { + desc: "record not found", + fqdn: "_acme-challenge.huu.com.", + zoneID: "test-zone", + txtRecords: []TXTRecord{ + { + ID: 1, + RecordID: "Record-A", + Host: "_acme-challenge.example.org", + Value: "txtTXTtxtTXTtxtTXTtxtTXT", + Type: "TXT", + LineID: 6, + TTL: 30, + }, + { + ID: 2, + RecordID: "Record-B", + Host: "_acme-challenge.example.com", + Value: "TXTtxtTXTtxtTXTtxtTXTtxt", + Type: "TXT", + LineID: 6, + TTL: 30, + }, + }, + response: &apiResponse{ + Code: 1, + }, + expected: result{error: true}, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + client := setupTest(t, handlerMock(http.MethodGet, test.response, test.txtRecords)) + + txtRecord, err := client.FindTxtRecord(context.Background(), test.zoneID, test.fqdn) + + if test.expected.error { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, test.expected.txtRecord, txtRecord) + } + }) + } +} + +func TestClient_AddTxtRecord(t *testing.T) { + testCases := []struct { + desc string + domain *Data + fqdn string + value string + ttl int + expected string + }{ + { + desc: "sub-domain", + domain: &Data{ + ID: "1", + Domain: "example.com.", + }, + fqdn: "_acme-challenge.foo.example.com.", + value: "txtTXTtxtTXTtxtTXTtxtTXT", + ttl: 30, + expected: `{"domain_id":1,"host":"_acme-challenge.foo","value":"txtTXTtxtTXTtxtTXTtxtTXT","type":"TXT","line_id":"1","ttl":"30"}`, + }, + { + desc: "main domain", + domain: &Data{ + ID: "2", + Domain: "example.com.", + }, + fqdn: "_acme-challenge.example.com.", + value: "TXTtxtTXTtxtTXTtxtTXTtxt", + ttl: 30, + expected: `{"domain_id":2,"host":"_acme-challenge","value":"TXTtxtTXTtxtTXTtxtTXTtxt","type":"TXT","line_id":"1","ttl":"30"}`, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + response := &apiResponse{ + Code: 1, + } + + client := setupTest(t, func(rw http.ResponseWriter, req *http.Request) { + assert.NotNil(t, req.Body) + content, err := io.ReadAll(req.Body) + require.NoError(t, err) + + assert.Equal(t, test.expected, string(bytes.TrimSpace(content))) + + handlerMock(http.MethodPost, response, nil).ServeHTTP(rw, req) + }) + + err := client.AddTxtRecord(context.Background(), test.domain, test.fqdn, test.value, test.ttl) + require.NoError(t, err) + }) + } +} diff --git a/providers/dns/cloudxns/internal/types.go b/providers/dns/cloudxns/internal/types.go new file mode 100644 index 000000000..c1b24e30c --- /dev/null +++ b/providers/dns/cloudxns/internal/types.go @@ -0,0 +1,28 @@ +package internal + +import "encoding/json" + +type apiResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data json.RawMessage `json:"data,omitempty"` +} + +// Data Domain information. +type Data struct { + ID string `json:"id"` + Domain string `json:"domain"` + TTL int `json:"ttl,omitempty"` +} + +// TXTRecord a TXT record. +type TXTRecord struct { + ID int `json:"domain_id,omitempty"` + RecordID string `json:"record_id,omitempty"` + + Host string `json:"host"` + Value string `json:"value"` + Type string `json:"type"` + LineID int `json:"line_id,string"` + TTL int `json:"ttl,string"` +} diff --git a/providers/dns/com35/com35.go b/providers/dns/com35/com35.go deleted file mode 100644 index 4a9de3a18..000000000 --- a/providers/dns/com35/com35.go +++ /dev/null @@ -1,104 +0,0 @@ -// Package com35 implements a DNS provider for solving the DNS-01 challenge using 35.com/三五互联. -package com35 - -import ( - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/westcn" -) - -// Environment variables names. -const ( - envNamespace = "COM35_" - - EnvUsername = envNamespace + "USERNAME" - EnvPassword = envNamespace + "PASSWORD" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -const defaultBaseURL = "https://api.35.cn/api/v2" - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config = westcn.Config - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, 60), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - prv challenge.ProviderTimeout -} - -// NewDNSProvider returns a DNSProvider instance configured for 35.com/三五互联. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvUsername, EnvPassword) - if err != nil { - return nil, fmt.Errorf("35com: %w", err) - } - - config := NewDefaultConfig() - config.Username = values[EnvUsername] - config.Password = values[EnvPassword] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for 35.com/三五互联. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("35com: the configuration of the DNS provider is nil") - } - - provider, err := westcn.NewDNSProviderConfig(config, defaultBaseURL) - if err != nil { - return nil, fmt.Errorf("35com: %w", err) - } - - return &DNSProvider{prv: provider}, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - err := d.prv.Present(domain, token, keyAuth) - if err != nil { - return fmt.Errorf("35com: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - err := d.prv.CleanUp(domain, token, keyAuth) - if err != nil { - return fmt.Errorf("35com: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.prv.Timeout() -} diff --git a/providers/dns/com35/com35.toml b/providers/dns/com35/com35.toml deleted file mode 100644 index 386ee0043..000000000 --- a/providers/dns/com35/com35.toml +++ /dev/null @@ -1,24 +0,0 @@ -Name = "35.com/三五互联" -Description = '''''' -URL = "https://www.35.cn/" -Code = "com35" -Since = "v4.31.0" - -Example = ''' -COM35_USERNAME="xxx" \ -COM35_PASSWORD="yyy" \ -lego --dns com35 -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - COM35_USERNAME = "Username" - COM35_PASSWORD = "API password" - [Configuration.Additional] - COM35_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" - COM35_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - COM35_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" - COM35_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://api.35.cn/CustomerCenter/doc/domain_v2.html" diff --git a/providers/dns/com35/com35_test.go b/providers/dns/com35/com35_test.go deleted file mode 100644 index 78fd8f829..000000000 --- a/providers/dns/com35/com35_test.go +++ /dev/null @@ -1,144 +0,0 @@ -package com35 - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvUsername, EnvPassword).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvUsername: "user", - EnvPassword: "secret", - }, - }, - { - desc: "missing username", - envVars: map[string]string{ - EnvUsername: "", - EnvPassword: "secret", - }, - expected: "35com: some credentials information are missing: COM35_USERNAME", - }, - { - desc: "missing password", - envVars: map[string]string{ - EnvUsername: "user", - EnvPassword: "", - }, - expected: "35com: some credentials information are missing: COM35_PASSWORD", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "35com: some credentials information are missing: COM35_USERNAME,COM35_PASSWORD", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.prv) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - username string - password string - expected string - }{ - { - desc: "success", - username: "user", - password: "secret", - }, - { - desc: "missing username", - password: "secret", - expected: "35com: credentials missing", - }, - { - desc: "missing password", - username: "user", - expected: "35com: credentials missing", - }, - { - desc: "missing credentials", - expected: "35com: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.Username = test.username - config.Password = test.password - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.prv) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/conoha/conoha.go b/providers/dns/conoha/conoha.go index f7658647c..32755b9f3 100644 --- a/providers/dns/conoha/conoha.go +++ b/providers/dns/conoha/conoha.go @@ -8,11 +8,9 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/conoha/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -30,8 +28,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Region string @@ -99,8 +95,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { identifier.HTTPClient = config.HTTPClient } - identifier.HTTPClient = clientdebug.Wrap(identifier.HTTPClient) - auth := internal.Auth{ TenantID: config.TenantID, PasswordCredentials: internal.PasswordCredentials{ @@ -123,8 +117,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{config: config, client: client}, nil } diff --git a/providers/dns/conoha/conoha.toml b/providers/dns/conoha/conoha.toml index be90acb0d..417663dbb 100644 --- a/providers/dns/conoha/conoha.toml +++ b/providers/dns/conoha/conoha.toml @@ -1,4 +1,4 @@ -Name = "ConoHa v2" +Name = "ConoHa" Description = '''''' URL = "https://www.conoha.jp/" Code = "conoha" @@ -8,7 +8,7 @@ Example = ''' CONOHA_TENANT_ID=487727e3921d44e3bfe7ebb337bf085e \ CONOHA_API_USERNAME=xxxx \ CONOHA_API_PASSWORD=yyyy \ -lego --dns conoha -d '*.example.com' -d example.com run +lego --email you@example.com --dns conoha --domains my.example.org run ''' [Configuration] @@ -17,11 +17,11 @@ lego --dns conoha -d '*.example.com' -d example.com run CONOHA_API_USERNAME = "The API username" CONOHA_API_PASSWORD = "The API password" [Configuration.Additional] - CONOHA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - CONOHA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - CONOHA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" - CONOHA_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - CONOHA_REGION = "The region (Default: tyo1)" + CONOHA_POLLING_INTERVAL = "Time between DNS propagation check" + CONOHA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + CONOHA_TTL = "The TTL of the TXT record used for the DNS challenge" + CONOHA_HTTP_TIMEOUT = "API request timeout" + CONOHA_REGION = "The region" [Links] - API = "https://doc.conoha.jp/reference/api-vps2/api-dns-vps2" + API = "https://www.conoha.jp/docs/" diff --git a/providers/dns/conoha/conoha_test.go b/providers/dns/conoha/conoha_test.go index c1c445d48..9db5ba79f 100644 --- a/providers/dns/conoha/conoha_test.go +++ b/providers/dns/conoha/conoha_test.go @@ -72,7 +72,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -156,7 +155,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -170,7 +168,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/conoha/internal/client.go b/providers/dns/conoha/internal/client.go index 2f039489b..87fbe5a0b 100644 --- a/providers/dns/conoha/internal/client.go +++ b/providers/dns/conoha/internal/client.go @@ -25,7 +25,7 @@ type Client struct { } // NewClient returns a client instance logged into the ConoHa service. -func NewClient(region, token string) (*Client, error) { +func NewClient(region string, token string) (*Client, error) { baseURL, err := url.Parse(fmt.Sprintf(dnsServiceBaseURL, region)) if err != nil { return nil, err @@ -54,7 +54,7 @@ func (c *Client) GetDomainID(ctx context.Context, domainName string) (string, er return "", fmt.Errorf("no such domain: %s", domainName) } -// https://doc.conoha.jp/reference/api-vps2/api-dns-vps2/paas-dns-list-domains-v2/?btn_id=reference-api-vps2--sidebar_reference-paas-dns-list-domains-v2 +// https://www.conoha.jp/docs/paas-dns-list-domains.php func (c *Client) getDomains(ctx context.Context) (*DomainListResponse, error) { endpoint := c.baseURL.JoinPath("v1", "domains") @@ -89,7 +89,7 @@ func (c *Client) GetRecordID(ctx context.Context, domainID, recordName, recordTy return "", errors.New("no such record") } -// https://doc.conoha.jp/reference/api-vps2/api-dns-vps2/paas-dns-list-records-in-a-domain-v2/?btn_id=reference-paas-dns-list-domains-v2--sidebar_reference-paas-dns-list-records-in-a-domain-v2 +// https://www.conoha.jp/docs/paas-dns-list-records-in-a-domain.php func (c *Client) getRecords(ctx context.Context, domainID string) (*RecordListResponse, error) { endpoint := c.baseURL.JoinPath("v1", "domains", domainID, "records") @@ -114,7 +114,7 @@ func (c *Client) CreateRecord(ctx context.Context, domainID string, record Recor return err } -// https://doc.conoha.jp/reference/api-vps2/api-dns-vps2/paas-dns-create-record-v2/?btn_id=reference-paas-dns-list-records-in-a-domain-v2--sidebar_reference-paas-dns-create-record-v2 +// https://www.conoha.jp/docs/paas-dns-create-record.php func (c *Client) createRecord(ctx context.Context, domainID string, record Record) (*Record, error) { endpoint := c.baseURL.JoinPath("v1", "domains", domainID, "records") @@ -124,7 +124,6 @@ func (c *Client) createRecord(ctx context.Context, domainID string, record Recor } newRecord := &Record{} - err = c.do(req, newRecord) if err != nil { return nil, err @@ -134,7 +133,7 @@ func (c *Client) createRecord(ctx context.Context, domainID string, record Recor } // DeleteRecord removes specified record. -// https://doc.conoha.jp/reference/api-vps2/api-dns-vps2/paas-dns-delete-a-record-v2/?btn_id=reference-paas-dns-create-record-v2--sidebar_reference-paas-dns-delete-a-record-v2 +// https://www.conoha.jp/docs/paas-dns-delete-a-record.php func (c *Client) DeleteRecord(ctx context.Context, domainID, recordID string) error { endpoint := c.baseURL.JoinPath("v1", "domains", domainID, "records", recordID) diff --git a/providers/dns/conoha/internal/client_test.go b/providers/dns/conoha/internal/client_test.go index 5e06ffc1d..bc27ec212 100644 --- a/providers/dns/conoha/internal/client_test.go +++ b/providers/dns/conoha/internal/client_test.go @@ -2,6 +2,7 @@ package internal import ( "bytes" + "context" "fmt" "io" "net/http" @@ -11,26 +12,60 @@ import ( "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("tyo1", "secret") - if err != nil { - return nil, err - } +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - With("X-Auth-Token", "secret")) + client, err := NewClient("tyo1", "secret") + require.NoError(t, err) + + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client, mux +} + +func writeFixtureHandler(method, filename string) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + writeFixture(rw, filename) + } +} + +func writeBodyHandler(method, content string) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + _, err := fmt.Fprint(rw, content) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + } +} + +func writeFixture(rw http.ResponseWriter, filename string) { + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, _ = io.Copy(rw, file) } func TestClient_GetDomainID(t *testing.T) { @@ -42,36 +77,36 @@ func TestClient_GetDomainID(t *testing.T) { testCases := []struct { desc string domainName string - response string + handler http.HandlerFunc expected expected }{ { desc: "success", domainName: "domain1.com.", - response: "domains_GET.json", + handler: writeFixtureHandler(http.MethodGet, "domains_GET.json"), expected: expected{domainID: "09494b72-b65b-4297-9efb-187f65a0553e"}, }, { desc: "non existing domain", domainName: "domain1.com.", - response: "empty.json", + handler: writeBodyHandler(http.MethodGet, "{}"), expected: expected{error: true}, }, { desc: "marshaling error", domainName: "domain1.com.", - response: "empty.json", + handler: writeBodyHandler(http.MethodGet, "[]"), expected: expected{error: true}, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - client := mockBuilder(). - Route("GET /v1/domains", servermock.ResponseFromFixture(test.response)). - Build(t) + client, mux := setupTest(t) - domainID, err := client.GetDomainID(t.Context(), test.domainName) + mux.Handle("/v1/domains", test.handler) + + domainID, err := client.GetDomainID(context.Background(), test.domainName) if test.expected.error { require.Error(t, err) @@ -92,12 +127,16 @@ func TestClient_CreateRecord(t *testing.T) { { desc: "success", handler: func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + raw, err := io.ReadAll(req.Body) if err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } - defer func() { _ = req.Body.Close() }() if string(bytes.TrimSpace(raw)) != `{"name":"lego.com.","type":"TXT","data":"txtTXTtxt","ttl":300}` { @@ -105,21 +144,18 @@ func TestClient_CreateRecord(t *testing.T) { return } - file, err := os.Open(filepath.Join("fixtures", "domains-records_POST.json")) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - - defer func() { _ = file.Close() }() - - _, _ = io.Copy(rw, file) + writeFixture(rw, "domains-records_POST.json") }, assert: require.NoError, }, { desc: "bad request", handler: func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + http.Error(rw, "OOPS", http.StatusBadRequest) }, assert: require.Error, @@ -128,9 +164,9 @@ func TestClient_CreateRecord(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - client := mockBuilder(). - Route("POST /v1/domains/lego/records", test.handler). - Build(t) + client, mux := setupTest(t) + + mux.Handle("/v1/domains/lego/records", test.handler) domainID := "lego" @@ -141,30 +177,36 @@ func TestClient_CreateRecord(t *testing.T) { TTL: 300, } - err := client.CreateRecord(t.Context(), domainID, record) + err := client.CreateRecord(context.Background(), domainID, record) test.assert(t, err) }) } } func TestClient_GetRecordID(t *testing.T) { - client := mockBuilder(). - Route("GET /v1/domains/89acac79-38e7-497d-807c-a011e1310438/records", - servermock.ResponseFromFixture("domains-records_GET.json")). - Build(t) + client, mux := setupTest(t) - recordID, err := client.GetRecordID(t.Context(), "89acac79-38e7-497d-807c-a011e1310438", "www.example.com.", "A", "15.185.172.153") + mux.HandleFunc("/v1/domains/89acac79-38e7-497d-807c-a011e1310438/records", + writeFixtureHandler(http.MethodGet, "domains-records_GET.json")) + + recordID, err := client.GetRecordID(context.Background(), "89acac79-38e7-497d-807c-a011e1310438", "www.example.com.", "A", "15.185.172.153") require.NoError(t, err) assert.Equal(t, "2e32e609-3a4f-45ba-bdef-e50eacd345ad", recordID) } func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /v1/domains/89acac79-38e7-497d-807c-a011e1310438/records/2e32e609-3a4f-45ba-bdef-e50eacd345ad", - servermock.ResponseFromFixture("domains-records_GET.json")). - Build(t) + client, mux := setupTest(t) - err := client.DeleteRecord(t.Context(), "89acac79-38e7-497d-807c-a011e1310438", "2e32e609-3a4f-45ba-bdef-e50eacd345ad") + mux.HandleFunc("/v1/domains/89acac79-38e7-497d-807c-a011e1310438/records/2e32e609-3a4f-45ba-bdef-e50eacd345ad", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodDelete { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + rw.WriteHeader(http.StatusOK) + }) + + err := client.DeleteRecord(context.Background(), "89acac79-38e7-497d-807c-a011e1310438", "2e32e609-3a4f-45ba-bdef-e50eacd345ad") require.NoError(t, err) } diff --git a/providers/dns/conoha/internal/fixtures/empty.json b/providers/dns/conoha/internal/fixtures/empty.json deleted file mode 100644 index 0967ef424..000000000 --- a/providers/dns/conoha/internal/fixtures/empty.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/providers/dns/conoha/internal/identity.go b/providers/dns/conoha/internal/identity.go index 54fc46bc5..995d55bb6 100644 --- a/providers/dns/conoha/internal/identity.go +++ b/providers/dns/conoha/internal/identity.go @@ -33,7 +33,7 @@ func NewIdentifier(region string) (*Identifier, error) { } // GetToken gets valid token information. -// https://doc.conoha.jp/reference/api-vps2/api-identity-vps2/identity-post_tokens-v2/?btn_id=reference-paas-dns-delete-a-record-v2--sidebar_reference-identity-post_tokens-v2 +// https://www.conoha.jp/docs/identity-post_tokens.php func (c *Identifier) GetToken(ctx context.Context, auth Auth) (*IdentityResponse, error) { endpoint := c.baseURL.JoinPath("v2.0", "tokens") diff --git a/providers/dns/conoha/internal/identity_test.go b/providers/dns/conoha/internal/identity_test.go index 0bd4c936a..027c7f2c7 100644 --- a/providers/dns/conoha/internal/identity_test.go +++ b/providers/dns/conoha/internal/identity_test.go @@ -1,33 +1,28 @@ package internal import ( + "context" + "net/http" "net/http/httptest" "net/url" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func setupIdentifier(server *httptest.Server) (*Identifier, error) { +func TestNewClient(t *testing.T) { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + identifier, err := NewIdentifier("tyo1") - if err != nil { - return nil, err - } + require.NoError(t, err) identifier.HTTPClient = server.Client() identifier.baseURL, _ = url.Parse(server.URL) - return identifier, nil -} - -func TestNewClient(t *testing.T) { - identifier := servermock.NewBuilder[*Identifier](setupIdentifier, - servermock.CheckHeader().WithJSONHeaders(), - ). - Route("POST /v2.0/tokens", servermock.ResponseFromFixture("tokens_POST.json")). - Build(t) + mux.HandleFunc("/v2.0/tokens", writeFixtureHandler(http.MethodPost, "tokens_POST.json")) auth := Auth{ TenantID: "487727e3921d44e3bfe7ebb337bf085e", @@ -37,7 +32,7 @@ func TestNewClient(t *testing.T) { }, } - token, err := identifier.GetToken(t.Context(), auth) + token, err := identifier.GetToken(context.Background(), auth) require.NoError(t, err) expected := &IdentityResponse{Access: Access{Token: Token{ID: "sample00d88246078f2bexample788f7"}}} diff --git a/providers/dns/conohav3/conohav3.go b/providers/dns/conohav3/conohav3.go deleted file mode 100644 index c1eace827..000000000 --- a/providers/dns/conohav3/conohav3.go +++ /dev/null @@ -1,203 +0,0 @@ -// Package conohav3 implements a DNS provider for solving the DNS-01 challenge using ConoHa VPS Ver 3.0 DNS. -package conohav3 - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/conohav3/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "CONOHAV3_" - - EnvRegion = envNamespace + "REGION" - EnvTenantID = envNamespace + "TENANT_ID" - EnvAPIUserID = envNamespace + "API_USER_ID" - EnvAPIPassword = envNamespace + "API_PASSWORD" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - Region string - TenantID string - UserID string - Password string - TTL int - PropagationTimeout time.Duration - PollingInterval time.Duration - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - Region: env.GetOrDefaultString(EnvRegion, "c3j1"), - TTL: env.GetOrDefaultInt(EnvTTL, 60), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for ConoHa DNS. -// Credentials must be passed in the environment variables: -// CONOHAV3_TENANT_ID, CONOHAV3_API_USER_ID, CONOHAV3_API_PASSWORD. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvTenantID, EnvAPIUserID, EnvAPIPassword) - if err != nil { - return nil, fmt.Errorf("conohav3: %w", err) - } - - config := NewDefaultConfig() - config.TenantID = values[EnvTenantID] - config.UserID = values[EnvAPIUserID] - config.Password = values[EnvAPIPassword] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for ConoHa DNS. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("conohav3: the configuration of the DNS provider is nil") - } - - if config.TenantID == "" || config.UserID == "" || config.Password == "" { - return nil, errors.New("conohav3: some credentials information are missing") - } - - identifier, err := internal.NewIdentifier(config.Region) - if err != nil { - return nil, fmt.Errorf("conohav3: failed to create identity client: %w", err) - } - - if config.HTTPClient != nil { - identifier.HTTPClient = config.HTTPClient - } - - identifier.HTTPClient = clientdebug.Wrap(identifier.HTTPClient) - - auth := internal.Auth{ - Identity: internal.Identity{ - Methods: []string{"password"}, - Password: internal.Password{ - User: internal.User{ - ID: config.UserID, - Password: config.Password, - }, - }, - }, - Scope: internal.Scope{ - Project: internal.Project{ - ID: config.TenantID, - }, - }, - } - - token, err := identifier.GetToken(context.Background(), auth) - if err != nil { - return nil, fmt.Errorf("conohav3: failed to log in: %w", err) - } - - client, err := internal.NewClient(config.Region, token) - if err != nil { - return nil, fmt.Errorf("conohav3: failed to create client: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{config: config, client: client}, nil -} - -// Present creates a TXT record to fulfill the dns-01 challenge. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("conohav3: could not find zone for domain %q: %w", domain, err) - } - - ctx := context.Background() - - id, err := d.client.GetDomainID(ctx, authZone) - if err != nil { - return fmt.Errorf("conohav3: failed to get domain ID: %w", err) - } - - record := internal.Record{ - Name: info.EffectiveFQDN, - Type: "TXT", - Data: info.Value, - TTL: d.config.TTL, - } - - err = d.client.CreateRecord(ctx, id, record) - if err != nil { - return fmt.Errorf("conohav3: failed to create record: %w", err) - } - - return nil -} - -// CleanUp clears ConoHa DNS TXT record. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("conohav3: could not find zone for domain %q: %w", domain, err) - } - - ctx := context.Background() - - domID, err := d.client.GetDomainID(ctx, authZone) - if err != nil { - return fmt.Errorf("conohav3: failed to get domain ID: %w", err) - } - - recID, err := d.client.GetRecordID(ctx, domID, info.EffectiveFQDN, "TXT", info.Value) - if err != nil { - return fmt.Errorf("conohav3: failed to get record ID: %w", err) - } - - err = d.client.DeleteRecord(ctx, domID, recID) - if err != nil { - return fmt.Errorf("conohav3: failed to delete record: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/conohav3/conohav3.toml b/providers/dns/conohav3/conohav3.toml deleted file mode 100644 index e2c80259d..000000000 --- a/providers/dns/conohav3/conohav3.toml +++ /dev/null @@ -1,27 +0,0 @@ -Name = "ConoHa v3" -Description = '''''' -URL = "https://www.conoha.jp/" -Code = "conohav3" -Since = "v4.24.0" - -Example = ''' -CONOHAV3_TENANT_ID=487727e3921d44e3bfe7ebb337bf085e \ -CONOHAV3_API_USER_ID=xxxx \ -CONOHAV3_API_PASSWORD=yyyy \ -lego --dns conohav3 -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - CONOHAV3_TENANT_ID = "Tenant ID" - CONOHAV3_API_USER_ID = "The API user ID" - CONOHAV3_API_PASSWORD = "The API password" - [Configuration.Additional] - CONOHAV3_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - CONOHAV3_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - CONOHAV3_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" - CONOHAV3_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - CONOHAV3_REGION = "The region (Default: c3j1)" - -[Links] - API = "https://doc.conoha.jp/reference/api-vps3/api-dns-vps3/" diff --git a/providers/dns/conohav3/conohav3_test.go b/providers/dns/conohav3/conohav3_test.go deleted file mode 100644 index d68ea3ebb..000000000 --- a/providers/dns/conohav3/conohav3_test.go +++ /dev/null @@ -1,181 +0,0 @@ -package conohav3 - -import ( - "testing" - "time" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest( - EnvTenantID, - EnvAPIUserID, - EnvAPIPassword). - WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "complete credentials, but login failed", - envVars: map[string]string{ - EnvTenantID: "tenant_id", - EnvAPIUserID: "api_user_id", - EnvAPIPassword: "api_password", - }, - expected: `conohav3: failed to log in: unexpected status code: [status code: 400] body: {"code": 400, "error": "user does not exist"}`, - }, - { - desc: "missing credentials", - envVars: map[string]string{ - EnvTenantID: "", - EnvAPIUserID: "", - EnvAPIPassword: "", - }, - expected: "conohav3: some credentials information are missing: CONOHAV3_TENANT_ID,CONOHAV3_API_USER_ID,CONOHAV3_API_PASSWORD", - }, - { - desc: "missing tenant id", - envVars: map[string]string{ - EnvTenantID: "", - EnvAPIUserID: "api_user_id", - EnvAPIPassword: "api_password", - }, - expected: "conohav3: some credentials information are missing: CONOHAV3_TENANT_ID", - }, - { - desc: "missing api user id", - envVars: map[string]string{ - EnvTenantID: "tenant_id", - EnvAPIUserID: "", - EnvAPIPassword: "api_password", - }, - expected: "conohav3: some credentials information are missing: CONOHAV3_API_USER_ID", - }, - { - desc: "missing api password", - envVars: map[string]string{ - EnvTenantID: "tenant_id", - EnvAPIUserID: "api_user_id", - EnvAPIPassword: "", - }, - expected: "conohav3: some credentials information are missing: CONOHAV3_API_PASSWORD", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - expected string - tenant string - userid string - password string - }{ - { - desc: "complete credentials, but login failed", - expected: `conohav3: failed to log in: unexpected status code: [status code: 400] body: {"code": 400, "error": "user does not exist"}`, - tenant: "tenant_id", - userid: "api_user_id", - password: "api_password", - }, - { - desc: "missing credentials", - expected: "conohav3: some credentials information are missing", - }, - { - desc: "missing tenant id", - expected: "conohav3: some credentials information are missing", - userid: "api_user_id", - password: "api_password", - }, - { - desc: "missing api user id", - expected: "conohav3: some credentials information are missing", - tenant: "tenant_id", - password: "api_password", - }, - { - desc: "missing api password", - expected: "conohav3: some credentials information are missing", - tenant: "tenant_id", - userid: "api_user_id", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.TenantID = test.tenant - config.UserID = test.userid - config.Password = test.password - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - time.Sleep(1 * time.Second) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/conohav3/internal/client.go b/providers/dns/conohav3/internal/client.go deleted file mode 100644 index 2a9e7c2bc..000000000 --- a/providers/dns/conohav3/internal/client.go +++ /dev/null @@ -1,204 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" -) - -const dnsServiceBaseURL = "https://dns-service.%s.conoha.io" - -// Client is a ConoHa API client. -type Client struct { - token string - - baseURL *url.URL - HTTPClient *http.Client -} - -// NewClient returns a client instance logged into the ConoHa service. -func NewClient(region, token string) (*Client, error) { - baseURL, err := url.Parse(fmt.Sprintf(dnsServiceBaseURL, region)) - if err != nil { - return nil, err - } - - return &Client{ - token: token, - baseURL: baseURL, - HTTPClient: &http.Client{Timeout: 5 * time.Second}, - }, nil -} - -// GetDomainID returns an ID of specified domain. -func (c *Client) GetDomainID(ctx context.Context, domainName string) (string, error) { - domainList, err := c.getDomains(ctx) - if err != nil { - return "", err - } - - for _, domain := range domainList.Domains { - if domain.Name == domainName { - return domain.UUID, nil - } - } - - return "", fmt.Errorf("no such domain: %s", domainName) -} - -// https://doc.conoha.jp/reference/api-vps3/api-dns-vps3/dnsaas-get_domains_list-v3/?btn_id=reference-api-vps3--sidebar_reference-dnsaas-get_domains_list-v3 -func (c *Client) getDomains(ctx context.Context) (*DomainListResponse, error) { - endpoint := c.baseURL.JoinPath("v1", "domains") - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - domainList := &DomainListResponse{} - - err = c.do(req, domainList) - if err != nil { - return nil, err - } - - return domainList, nil -} - -// GetRecordID returns an ID of specified record. -func (c *Client) GetRecordID(ctx context.Context, domainID, recordName, recordType, data string) (string, error) { - recordList, err := c.getRecords(ctx, domainID) - if err != nil { - return "", err - } - - for _, record := range recordList.Records { - if record.Name == recordName && record.Type == recordType && record.Data == data { - return record.UUID, nil - } - } - - return "", errors.New("no such record") -} - -// https://doc.conoha.jp/reference/api-vps3/api-dns-vps3/dnsaas-get_records_list-v3/?btn_id=reference-dnsaas-get_domains_list-v3--sidebar_reference-dnsaas-get_records_list-v3 -func (c *Client) getRecords(ctx context.Context, domainID string) (*RecordListResponse, error) { - endpoint := c.baseURL.JoinPath("v1", "domains", domainID, "records") - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - recordList := &RecordListResponse{} - - err = c.do(req, recordList) - if err != nil { - return nil, err - } - - return recordList, nil -} - -// CreateRecord adds new record. -func (c *Client) CreateRecord(ctx context.Context, domainID string, record Record) error { - _, err := c.createRecord(ctx, domainID, record) - return err -} - -// https://doc.conoha.jp/reference/api-vps3/api-dns-vps3/dnsaas-create_record-v3/?btn_id=reference-dnsaas-get_records_list-v3--sidebar_reference-dnsaas-create_record-v3 -func (c *Client) createRecord(ctx context.Context, domainID string, record Record) (*Record, error) { - endpoint := c.baseURL.JoinPath("v1", "domains", domainID, "records") - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) - if err != nil { - return nil, err - } - - newRecord := &Record{} - - err = c.do(req, newRecord) - if err != nil { - return nil, err - } - - return newRecord, nil -} - -// DeleteRecord removes specified record. -// https://doc.conoha.jp/reference/api-vps3/api-dns-vps3/dnsaas-delete_record-v3/?btn_id=reference-dnsaas-create_record-v3--sidebar_reference-dnsaas-delete_record-v3 -func (c *Client) DeleteRecord(ctx context.Context, domainID, recordID string) error { - endpoint := c.baseURL.JoinPath("v1", "domains", domainID, "records", recordID) - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return err - } - - return c.do(req, nil) -} - -func (c *Client) do(req *http.Request, result any) error { - if c.token != "" { - req.Header.Set("X-Auth-Token", c.token) - } - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { - return errutils.NewUnexpectedResponseStatusCodeError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} diff --git a/providers/dns/conohav3/internal/client_test.go b/providers/dns/conohav3/internal/client_test.go deleted file mode 100644 index 66babae49..000000000 --- a/providers/dns/conohav3/internal/client_test.go +++ /dev/null @@ -1,171 +0,0 @@ -package internal - -import ( - "bytes" - "fmt" - "io" - "net/http" - "net/http/httptest" - "net/url" - "os" - "path/filepath" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("c3j1", "secret") - if err != nil { - return nil, err - } - - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(). - With("X-Auth-Token", "secret")) -} - -func TestClient_GetDomainID(t *testing.T) { - type expected struct { - domainID string - error bool - } - - testCases := []struct { - desc string - domainName string - response string - expected expected - }{ - { - desc: "success", - domainName: "domain1.com.", - response: "domains_GET.json", - expected: expected{domainID: "09494b72-b65b-4297-9efb-187f65a0553e"}, - }, - { - desc: "non existing domain", - domainName: "domain1.com.", - response: "empty.json", - expected: expected{error: true}, - }, - { - desc: "marshaling error", - domainName: "domain1.com.", - response: "empty.json", - expected: expected{error: true}, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - client := mockBuilder(). - Route("GET /v1/domains", servermock.ResponseFromFixture(test.response)). - Build(t) - - domainID, err := client.GetDomainID(t.Context(), test.domainName) - - if test.expected.error { - require.Error(t, err) - } else { - require.NoError(t, err) - assert.Equal(t, test.expected.domainID, domainID) - } - }) - } -} - -func TestClient_CreateRecord(t *testing.T) { - testCases := []struct { - desc string - handler http.HandlerFunc - assert require.ErrorAssertionFunc - }{ - { - desc: "success", - handler: func(rw http.ResponseWriter, req *http.Request) { - raw, err := io.ReadAll(req.Body) - if err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) - return - } - - defer func() { _ = req.Body.Close() }() - - if string(bytes.TrimSpace(raw)) != `{"name":"lego.com.","type":"TXT","data":"txtTXTtxt","ttl":300}` { - http.Error(rw, fmt.Sprintf("invalid request body: %s", string(raw)), http.StatusBadRequest) - return - } - - file, err := os.Open(filepath.Join("fixtures", "domains-records_POST.json")) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - - defer func() { _ = file.Close() }() - - _, _ = io.Copy(rw, file) - }, - assert: require.NoError, - }, - { - desc: "bad request", - handler: func(rw http.ResponseWriter, req *http.Request) { - http.Error(rw, "OOPS", http.StatusBadRequest) - }, - assert: require.Error, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - client := mockBuilder(). - Route("POST /v1/domains/lego/records", test.handler). - Build(t) - - domainID := "lego" - - record := Record{ - Name: "lego.com.", - Type: "TXT", - Data: "txtTXTtxt", - TTL: 300, - } - - err := client.CreateRecord(t.Context(), domainID, record) - test.assert(t, err) - }) - } -} - -func TestClient_GetRecordID(t *testing.T) { - client := mockBuilder(). - Route("GET /v1/domains/89acac79-38e7-497d-807c-a011e1310438/records", - servermock.ResponseFromFixture("domains-records_GET.json")). - Build(t) - - recordID, err := client.GetRecordID(t.Context(), "89acac79-38e7-497d-807c-a011e1310438", "www.example.com.", "A", "15.185.172.153") - require.NoError(t, err) - - assert.Equal(t, "2e32e609-3a4f-45ba-bdef-e50eacd345ad", recordID) -} - -func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /v1/domains/89acac79-38e7-497d-807c-a011e1310438/records/2e32e609-3a4f-45ba-bdef-e50eacd345ad", - servermock.ResponseFromFixture("domains-records_GET.json")). - Build(t) - - err := client.DeleteRecord(t.Context(), "89acac79-38e7-497d-807c-a011e1310438", "2e32e609-3a4f-45ba-bdef-e50eacd345ad") - require.NoError(t, err) -} diff --git a/providers/dns/conohav3/internal/fixtures/domains-records_GET.json b/providers/dns/conohav3/internal/fixtures/domains-records_GET.json deleted file mode 100644 index f982c1911..000000000 --- a/providers/dns/conohav3/internal/fixtures/domains-records_GET.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "records": [ - { - "uuid": "2e32e609-3a4f-45ba-bdef-e50eacd345ad", - "name": "www.example.com.", - "type": "A", - "ttl": 3600, - "created_at": "2012-11-02T19:56:26.000000", - "updated_at": "2012-11-04T13:22:36.000000", - "data": "15.185.172.153", - "domain_id": "89acac79-38e7-497d-807c-a011e1310438", - "version": 1, - "gslb_region": "JP", - "gslb_weight": 250, - "gslb_check": 12300 - }, - { - "uuid": "8e9ecf3e-fb92-4a3a-a8ae-7596f167bea3", - "name": "host1.example.com.", - "type": "A", - "ttl": 3600, - "created_at": "2012-11-04T13:57:50.000000", - "updated_at": null, - "data": "15.185.172.154", - "domain_id": "89acac79-38e7-497d-807c-a011e1310438", - "version": 1, - "gslb_region": "US", - "gslb_weight": 220, - "gslb_check": 12200 - }, - { - "uuid": "4ad19089-3e62-40f8-9482-17cc8ccb92cb", - "name": "web.example.com.", - "type": "CNAME", - "ttl": 3600, - "created_at": "2012-11-04T13:58:16.393735", - "updated_at": null, - "data": "www.example.com.", - "domain_id": "89acac79-38e7-497d-807c-a011e1310438", - "version": 1 - } - ] -} diff --git a/providers/dns/conohav3/internal/fixtures/domains-records_POST.json b/providers/dns/conohav3/internal/fixtures/domains-records_POST.json deleted file mode 100644 index d0f71c03e..000000000 --- a/providers/dns/conohav3/internal/fixtures/domains-records_POST.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "uuid": "2e32e609-3a4f-45ba-bdef-e50eacd345ad", - "name": "www.example.com.", - "type": "A", - "created_at": "2012-11-02T19:56:26.366792", - "updated_at": null, - "domain_id": "89acac79-38e7-497d-807c-a011e1310438", - "ttl": null, - "data": "192.0.2.3", - "gslb_check": 1, - "gslb_region": "JP", - "gslb_weight": 250 -} diff --git a/providers/dns/conohav3/internal/fixtures/domains_GET.json b/providers/dns/conohav3/internal/fixtures/domains_GET.json deleted file mode 100644 index 6f8603a57..000000000 --- a/providers/dns/conohav3/internal/fixtures/domains_GET.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "domains": [ - { - "uuid": "09494b72-b65b-4297-9efb-187f65a0553e", - "name": "domain1.com.", - "project_id": "cf661142-e577-40b5-b3eb-75795cdc0cd7", - "serial": 1701909248, - "ttl": 3600, - "email": "nsadmin1@example.org", - "created_at": "2023-12-07T00:34:08Z", - "updated_at": "2023-12-07T00:34:08Z" - }, - { - "uuid": "cf661142-e577-40b5-b3eb-75795cdc0cd7", - "name": "domain2.com.", - "project_id": "cf661144-e578-39b6-b4eb-75794cdc1cd8", - "serial": 1351800670, - "ttl": 7200, - "email": "nsadmin2@example.org", - "created_at": "2012-11-01T20:11:08Z", - "updated_at": "2012-12-01T20:11:08Z" - } - ], - "total_count": 1 -} diff --git a/providers/dns/conohav3/internal/fixtures/empty.json b/providers/dns/conohav3/internal/fixtures/empty.json deleted file mode 100644 index 0967ef424..000000000 --- a/providers/dns/conohav3/internal/fixtures/empty.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/providers/dns/conohav3/internal/identity.go b/providers/dns/conohav3/internal/identity.go deleted file mode 100644 index 6a9ad7f1e..000000000 --- a/providers/dns/conohav3/internal/identity.go +++ /dev/null @@ -1,71 +0,0 @@ -// internal/identity.go - -package internal - -import ( - "context" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" -) - -const identityBaseURL = "https://identity.%s.conoha.io" - -type Identifier struct { - baseURL *url.URL - HTTPClient *http.Client -} - -// NewIdentifier creates a new Identifier. -func NewIdentifier(region string) (*Identifier, error) { - baseURL, err := url.Parse(fmt.Sprintf(identityBaseURL, region)) - if err != nil { - return nil, err - } - - return &Identifier{ - baseURL: baseURL, - HTTPClient: &http.Client{Timeout: 5 * time.Second}, - }, nil -} - -// GetToken returns the x-subject-token from Identity API. -// https://doc.conoha.jp/reference/api-vps3/api-identity-vps3/identity-post_tokens-v3/?btn_id=reference-api-guideline-v3--sidebar_reference-identity-post_tokens-v3 -func (c *Identifier) GetToken(ctx context.Context, auth Auth) (string, error) { - endpoint := c.baseURL.JoinPath("v3", "auth", "tokens") - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, &IdentityRequest{Auth: auth}) - if err != nil { - return "", err - } - - return c.do(req) -} - -// do sends the request and returns the token from x-subject-token header. -func (c *Identifier) do(req *http.Request) (string, error) { - resp, err := c.HTTPClient.Do(req) - if err != nil { - return "", errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusCreated { - return "", errutils.NewUnexpectedResponseStatusCodeError(req, resp) - } - - token := resp.Header.Get("x-subject-token") - if token == "" { - return "", errors.New("x-subject-token header is missing in response") - } - - _, _ = io.Copy(io.Discard, resp.Body) - - return token, nil -} diff --git a/providers/dns/conohav3/internal/identity_test.go b/providers/dns/conohav3/internal/identity_test.go deleted file mode 100644 index d479a18d9..000000000 --- a/providers/dns/conohav3/internal/identity_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func setupIdentifier(server *httptest.Server) (*Identifier, error) { - identifier, err := NewIdentifier("c3j1") - if err != nil { - return nil, err - } - - identifier.HTTPClient = server.Client() - identifier.baseURL, _ = url.Parse(server.URL) - - return identifier, nil -} - -func TestGetToken_HeaderToken(t *testing.T) { - identifier := servermock.NewBuilder[*Identifier](setupIdentifier, - servermock.CheckHeader().WithJSONHeaders(), - ). - Route("POST /v3/auth/tokens", - servermock.ResponseFromFixture("empty.json"). - WithStatusCode(http.StatusCreated). - WithHeader("x-subject-token", "sample-header-token-123")). - Build(t) - - auth := Auth{ - Identity: Identity{ - Methods: []string{"password"}, - Password: Password{ - User: User{ - ID: "dummy-id", - Password: "dummy-password", - }, - }, - }, - Scope: Scope{ - Project: Project{ - ID: "dummy-project-id", - }, - }, - } - - token, err := identifier.GetToken(t.Context(), auth) - require.NoError(t, err) - - assert.Equal(t, "sample-header-token-123", token) -} diff --git a/providers/dns/conohav3/internal/types.go b/providers/dns/conohav3/internal/types.go deleted file mode 100644 index 99a162dd0..000000000 --- a/providers/dns/conohav3/internal/types.go +++ /dev/null @@ -1,65 +0,0 @@ -package internal - -// IdentityRequest is the top-level payload sent to the Identity v3. -type IdentityRequest struct { - Auth Auth `json:"auth"` -} - -// Auth authentication credentials (Identity) and scope (Scope). -type Auth struct { - Identity Identity `json:"identity"` - Scope Scope `json:"scope"` -} - -// Identity describes how the client will authenticate. -// In ConoHa v3.0, only support the "password" method. -type Identity struct { - Methods []string `json:"methods"` - Password Password `json:"password"` -} - -// Password nests the concrete user credentials used by the password auth method. -type Password struct { - User User `json:"user"` -} - -// User holds the API User ID and password that will be verified by the Identity service. -type User struct { - ID string `json:"id"` - Password string `json:"password"` -} - -// Scope specifies which tenant the issued token should be scoped to. -type Scope struct { - Project Project `json:"project"` -} - -// Project identifies the target tenant by UUID. -type Project struct { - ID string `json:"id"` -} - -// DomainListResponse is returned by `GET /v1/domains` and contains all DNS zones (domains) owned by the project. -type DomainListResponse struct { - Domains []Domain `json:"domains"` -} - -// Domain represents a single hosted DNS zone. -type Domain struct { - UUID string `json:"uuid"` - Name string `json:"name"` -} - -// RecordListResponse is returned by `GET /v1/domains/{domain_uuid}/records` and lists every record in the zone. -type RecordListResponse struct { - Records []Record `json:"records"` -} - -// Record represents a DNS record inside a zone. -type Record struct { - UUID string `json:"uuid,omitempty"` - Name string `json:"name"` - Type string `json:"type"` - Data string `json:"data"` - TTL int `json:"ttl"` -} diff --git a/providers/dns/constellix/constellix.go b/providers/dns/constellix/constellix.go index 777e93308..a7e81371d 100644 --- a/providers/dns/constellix/constellix.go +++ b/providers/dns/constellix/constellix.go @@ -10,11 +10,9 @@ import ( "strconv" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/constellix/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/hashicorp/go-retryablehttp" ) @@ -31,8 +29,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string @@ -97,7 +93,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { retryClient.HTTPClient = tr.Wrap(config.HTTPClient) retryClient.Backoff = backoff - client := internal.NewClient(clientdebug.Wrap(retryClient.StandardClient())) + client := internal.NewClient(retryClient.StandardClient()) return &DNSProvider{config: config, client: client}, nil } @@ -200,7 +196,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("constellix: failed to delete TXT records: %w", err) } - return nil } diff --git a/providers/dns/constellix/constellix.toml b/providers/dns/constellix/constellix.toml index 171a0de99..c5f7b2e45 100644 --- a/providers/dns/constellix/constellix.toml +++ b/providers/dns/constellix/constellix.toml @@ -7,7 +7,7 @@ Since = "v3.4.0" Example = ''' CONSTELLIX_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ CONSTELLIX_SECRET_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ -lego --dns constellix -d '*.example.com' -d example.com run +lego --email you@example.com --dns constellix --domains my.example.org run ''' [Configuration] @@ -15,10 +15,10 @@ lego --dns constellix -d '*.example.com' -d example.com run CONSTELLIX_API_KEY = "User API key" CONSTELLIX_SECRET_KEY = "User secret key" [Configuration.Additional] - CONSTELLIX_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" - CONSTELLIX_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - CONSTELLIX_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" - CONSTELLIX_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + CONSTELLIX_POLLING_INTERVAL = "Time between DNS propagation check" + CONSTELLIX_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + CONSTELLIX_TTL = "The TTL of the TXT record used for the DNS challenge" + CONSTELLIX_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://api-docs.constellix.com" diff --git a/providers/dns/constellix/constellix_test.go b/providers/dns/constellix/constellix_test.go index e38258292..e3a30caca 100644 --- a/providers/dns/constellix/constellix_test.go +++ b/providers/dns/constellix/constellix_test.go @@ -57,7 +57,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -130,7 +129,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -144,7 +142,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/constellix/internal/auth.go b/providers/dns/constellix/internal/auth.go index 9193572eb..1a136012d 100644 --- a/providers/dns/constellix/internal/auth.go +++ b/providers/dns/constellix/internal/auth.go @@ -28,7 +28,6 @@ func NewTokenTransport(apiKey, secretKey string) (*TokenTransport, error) { if apiKey == "" { return nil, errors.New("credentials missing: API key") } - if secretKey == "" { return nil, errors.New("credentials missing: secret key") } @@ -58,7 +57,6 @@ func (t *TokenTransport) transport() http.RoundTripper { if t.Transport != nil { return t.Transport } - return http.DefaultTransport } diff --git a/providers/dns/constellix/internal/domains.go b/providers/dns/constellix/internal/domains.go index fa7027f55..485f0d537 100644 --- a/providers/dns/constellix/internal/domains.go +++ b/providers/dns/constellix/internal/domains.go @@ -30,12 +30,10 @@ func (s *DomainService) GetAll(ctx context.Context, params *PaginationParameters if errQ != nil { return nil, errQ } - req.URL.RawQuery = v.Encode() } var domains []Domain - err = s.client.do(req, &domains) if err != nil { return nil, err @@ -80,7 +78,6 @@ func (s *DomainService) Search(ctx context.Context, filter searchFilter, value s req.URL.RawQuery = query.Encode() var domains []Domain - err = s.client.do(req, &domains) if err != nil { var nf *NotFound diff --git a/providers/dns/constellix/internal/domains_test.go b/providers/dns/constellix/internal/domains_test.go index 468db4613..1b0779b3d 100644 --- a/providers/dns/constellix/internal/domains_test.go +++ b/providers/dns/constellix/internal/domains_test.go @@ -1,57 +1,94 @@ package internal import ( + "context" + "io" + "net/http" "net/http/httptest" + "os" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient(server.Client()) - client.BaseURL = server.URL +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(), - ) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient(server.Client()) + client.BaseURL = server.URL + + return client, mux } func TestDomainService_GetAll(t *testing.T) { - client := mockBuilder(). - Route("GET /v1/domains", servermock.ResponseFromFixture("domains-GetAll.json")). - Build(t) + client, mux := setupTest(t) - data, err := client.Domains.GetAll(t.Context(), nil) + mux.HandleFunc("/v1/domains", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + file, err := os.Open("./fixtures/domains-GetAll.json") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + data, err := client.Domains.GetAll(context.Background(), nil) require.NoError(t, err) expected := []Domain{ - {ID: 273301, Name: "aaa.example", TypeID: 1, Version: 9, Status: "ACTIVE"}, - {ID: 273302, Name: "bbb.example", TypeID: 1, Version: 9, Status: "ACTIVE"}, - {ID: 273303, Name: "ccc.example", TypeID: 1, Version: 9, Status: "ACTIVE"}, - {ID: 273304, Name: "ddd.example", TypeID: 1, Version: 9, Status: "ACTIVE"}, + {ID: 273301, Name: "aaa.wtf", TypeID: 1, Version: 9, Status: "ACTIVE"}, + {ID: 273302, Name: "bbb.wtf", TypeID: 1, Version: 9, Status: "ACTIVE"}, + {ID: 273303, Name: "ccc.wtf", TypeID: 1, Version: 9, Status: "ACTIVE"}, + {ID: 273304, Name: "ddd.wtf", TypeID: 1, Version: 9, Status: "ACTIVE"}, } assert.Equal(t, expected, data) } func TestDomainService_Search(t *testing.T) { - client := mockBuilder(). - Route("GET /v1/domains/search", - servermock.ResponseFromFixture("domains-Search.json"), - servermock.CheckQueryParameter().Strict(). - With("exact", "example.com")). - Build(t) + client, mux := setupTest(t) - data, err := client.Domains.Search(t.Context(), Exact, "example.com") + mux.HandleFunc("/v1/domains/search", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + file, err := os.Open("./fixtures/domains-Search.json") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + data, err := client.Domains.Search(context.Background(), Exact, "lego.wtf") require.NoError(t, err) expected := []Domain{ - {ID: 273302, Name: "example.com", TypeID: 1, Version: 9, Status: "ACTIVE"}, + {ID: 273302, Name: "lego.wtf", TypeID: 1, Version: 9, Status: "ACTIVE"}, } assert.Equal(t, expected, data) diff --git a/providers/dns/constellix/internal/fixtures/domains-GetAll.json b/providers/dns/constellix/internal/fixtures/domains-GetAll.json index 8ccb4e52c..5ff2ad41d 100644 --- a/providers/dns/constellix/internal/fixtures/domains-GetAll.json +++ b/providers/dns/constellix/internal/fixtures/domains-GetAll.json @@ -1,7 +1,7 @@ [ { "id": 273301, - "name": "aaa.example", + "name": "aaa.wtf", "soa": { "primaryNameserver": "ns11.constellix.com.", "email": "dns.constellix.com.", @@ -36,7 +36,7 @@ }, { "id": 273302, - "name": "bbb.example", + "name": "bbb.wtf", "soa": { "primaryNameserver": "ns11.constellix.com.", "email": "dns.constellix.com.", @@ -71,7 +71,7 @@ }, { "id": 273303, - "name": "ccc.example", + "name": "ccc.wtf", "soa": { "primaryNameserver": "ns11.constellix.com.", "email": "dns.constellix.com.", @@ -106,7 +106,7 @@ }, { "id": 273304, - "name": "ddd.example", + "name": "ddd.wtf", "soa": { "primaryNameserver": "ns11.constellix.com.", "email": "dns.constellix.com.", diff --git a/providers/dns/constellix/internal/fixtures/domains-Search.json b/providers/dns/constellix/internal/fixtures/domains-Search.json index c33272515..5d018a39a 100644 --- a/providers/dns/constellix/internal/fixtures/domains-Search.json +++ b/providers/dns/constellix/internal/fixtures/domains-Search.json @@ -1,7 +1,7 @@ [ { "id": 273302, - "name": "example.com", + "name": "lego.wtf", "soa": { "primaryNameserver": "ns11.constellix.com.", "email": "dns.constellix.com.", diff --git a/providers/dns/constellix/internal/txtrecords.go b/providers/dns/constellix/internal/txtrecords.go index bd00d84b7..7880da4d2 100644 --- a/providers/dns/constellix/internal/txtrecords.go +++ b/providers/dns/constellix/internal/txtrecords.go @@ -32,7 +32,6 @@ func (s *TxtRecordService) Create(ctx context.Context, domainID int64, record Re } var records []Record - err = s.client.do(req, &records) if err != nil { return nil, err @@ -55,7 +54,6 @@ func (s *TxtRecordService) GetAll(ctx context.Context, domainID int64) ([]Record } var records []Record - err = s.client.do(req, &records) if err != nil { return nil, err @@ -78,7 +76,6 @@ func (s *TxtRecordService) Get(ctx context.Context, domainID, recordID int64) (* } var records Record - err = s.client.do(req, &records) if err != nil { return nil, err @@ -106,7 +103,6 @@ func (s *TxtRecordService) Update(ctx context.Context, domainID, recordID int64, } var msg SuccessMessage - err = s.client.do(req, &msg) if err != nil { return nil, err @@ -129,7 +125,6 @@ func (s *TxtRecordService) Delete(ctx context.Context, domainID, recordID int64) } var msg *SuccessMessage - err = s.client.do(req, &msg) if err != nil { return nil, err diff --git a/providers/dns/constellix/internal/txtrecords_test.go b/providers/dns/constellix/internal/txtrecords_test.go index 54d10dc38..7adc4af5c 100644 --- a/providers/dns/constellix/internal/txtrecords_test.go +++ b/providers/dns/constellix/internal/txtrecords_test.go @@ -1,22 +1,41 @@ package internal import ( + "context" "encoding/json" + "io" + "net/http" "os" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestTxtRecordService_Create(t *testing.T) { - client := mockBuilder(). - Route("POST /v1/domains/12345/records/txt", servermock.ResponseFromFixture("records-Create.json"), - servermock.CheckRequestJSONBody(`{"name":""}`)). - Build(t) + client, mux := setupTest(t) - records, err := client.TxtRecords.Create(t.Context(), 12345, RecordRequest{}) + mux.HandleFunc("/v1/domains/12345/records/txt", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + file, err := os.Open("./fixtures/records-Create.json") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + records, err := client.TxtRecords.Create(context.Background(), 12345, RecordRequest{}) require.NoError(t, err) recordsJSON, err := json.Marshal(records) @@ -29,11 +48,29 @@ func TestTxtRecordService_Create(t *testing.T) { } func TestTxtRecordService_GetAll(t *testing.T) { - client := mockBuilder(). - Route("GET /v1/domains/12345/records/txt", servermock.ResponseFromFixture("records-GetAll.json")). - Build(t) + client, mux := setupTest(t) - records, err := client.TxtRecords.GetAll(t.Context(), 12345) + mux.HandleFunc("/v1/domains/12345/records/txt", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + file, err := os.Open("./fixtures/records-GetAll.json") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + records, err := client.TxtRecords.GetAll(context.Background(), 12345) require.NoError(t, err) recordsJSON, err := json.Marshal(records) @@ -46,11 +83,29 @@ func TestTxtRecordService_GetAll(t *testing.T) { } func TestTxtRecordService_Get(t *testing.T) { - client := mockBuilder(). - Route("GET /v1/domains/12345/records/txt/6789", servermock.ResponseFromFixture("records-Get.json")). - Build(t) + client, mux := setupTest(t) - record, err := client.TxtRecords.Get(t.Context(), 12345, 6789) + mux.HandleFunc("/v1/domains/12345/records/txt/6789", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + file, err := os.Open("./fixtures/records-Get.json") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + record, err := client.TxtRecords.Get(context.Background(), 12345, 6789) require.NoError(t, err) expected := &Record{ @@ -76,12 +131,22 @@ func TestTxtRecordService_Get(t *testing.T) { } func TestTxtRecordService_Update(t *testing.T) { - client := mockBuilder(). - Route("PUT /v1/domains/12345/records/txt/6789", - servermock.RawStringResponse(`{"success":"Record updated successfully"}`)). - Build(t) + client, mux := setupTest(t) - msg, err := client.TxtRecords.Update(t.Context(), 12345, 6789, RecordRequest{}) + mux.HandleFunc("/v1/domains/12345/records/txt/6789", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPut { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + _, err := rw.Write([]byte(`{"success":"Record updated successfully"}`)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + msg, err := client.TxtRecords.Update(context.Background(), 12345, 6789, RecordRequest{}) require.NoError(t, err) expected := &SuccessMessage{Success: "Record updated successfully"} @@ -89,12 +154,22 @@ func TestTxtRecordService_Update(t *testing.T) { } func TestTxtRecordService_Delete(t *testing.T) { - client := mockBuilder(). - Route("DELETE /v1/domains/12345/records/txt/6789", - servermock.RawStringResponse(`{"success":"Record deleted successfully"}`)). - Build(t) + client, mux := setupTest(t) - msg, err := client.TxtRecords.Delete(t.Context(), 12345, 6789) + mux.HandleFunc("/v1/domains/12345/records/txt/6789", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodDelete { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + _, err := rw.Write([]byte(`{"success":"Record deleted successfully"}`)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + msg, err := client.TxtRecords.Delete(context.Background(), 12345, 6789) require.NoError(t, err) expected := &SuccessMessage{Success: "Record deleted successfully"} @@ -102,11 +177,29 @@ func TestTxtRecordService_Delete(t *testing.T) { } func TestTxtRecordService_Search(t *testing.T) { - client := mockBuilder(). - Route("GET /v1/domains/12345/records/txt/search", servermock.ResponseFromFixture("records-Search.json")). - Build(t) + client, mux := setupTest(t) - records, err := client.TxtRecords.Search(t.Context(), 12345, Exact, "test") + mux.HandleFunc("/v1/domains/12345/records/txt/search", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + file, err := os.Open("./fixtures/records-Search.json") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + records, err := client.TxtRecords.Search(context.Background(), 12345, Exact, "test") require.NoError(t, err) recordsJSON, err := json.Marshal(records) diff --git a/providers/dns/corenetworks/corenetworks.go b/providers/dns/corenetworks/corenetworks.go deleted file mode 100644 index cde58a2bf..000000000 --- a/providers/dns/corenetworks/corenetworks.go +++ /dev/null @@ -1,187 +0,0 @@ -package corenetworks - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/corenetworks/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "CORENETWORKS_" - - EnvLogin = envNamespace + "LOGIN" - EnvPassword = envNamespace + "PASSWORD" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - Login string - Password string - PropagationTimeout time.Duration - PollingInterval time.Duration - SequenceInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, 3600), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for Core-Networks. -// Credentials must be passed in the environment variables: CORENETWORKS_LOGIN, CORENETWORKS_PASSWORD. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvLogin, EnvPassword) - if err != nil { - return nil, fmt.Errorf("corenetworks: %w", err) - } - - config := NewDefaultConfig() - config.Login = values[EnvLogin] - config.Password = values[EnvPassword] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Bluecat DNS. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("corenetworks: the configuration of the DNS provider is nil") - } - - if config.Login == "" || config.Password == "" { - return nil, errors.New("corenetworks: credentials missing") - } - - client := internal.NewClient(config.Login, config.Password) - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{config: config, client: client}, nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -// Sequential All DNS challenges for this provider will be resolved sequentially. -// Returns the interval between each iteration. -func (d *DNSProvider) Sequential() time.Duration { - return d.config.SequenceInterval -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx, err := d.client.CreateAuthenticatedContext(context.Background()) - if err != nil { - return fmt.Errorf("create authentication token: %w", err) - } - - zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("corenetworks: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone) - if err != nil { - return fmt.Errorf("corenetworks: %w", err) - } - - record := internal.Record{ - Name: subDomain, - TTL: d.config.TTL, - Type: "TXT", - Data: info.Value, - } - - err = d.client.AddRecord(ctx, dns01.UnFqdn(zone), record) - if err != nil { - return fmt.Errorf("corenetworks: add record: %w", err) - } - - err = d.client.CommitRecords(ctx, dns01.UnFqdn(zone)) - if err != nil { - return fmt.Errorf("corenetworks: commit records: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx, err := d.client.CreateAuthenticatedContext(context.Background()) - if err != nil { - return fmt.Errorf("create authentication token: %w", err) - } - - zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("corenetworks: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone) - if err != nil { - return fmt.Errorf("corenetworks: %w", err) - } - - record := internal.Record{ - Name: subDomain, - TTL: d.config.TTL, - Type: "TXT", - Data: info.Value, - } - - err = d.client.DeleteRecords(ctx, dns01.UnFqdn(zone), record) - if err != nil { - return fmt.Errorf("corenetworks: delete records: %w", err) - } - - err = d.client.CommitRecords(ctx, dns01.UnFqdn(zone)) - if err != nil { - return fmt.Errorf("corenetworks: commit records: %w", err) - } - - return nil -} diff --git a/providers/dns/corenetworks/corenetworks.toml b/providers/dns/corenetworks/corenetworks.toml deleted file mode 100644 index 09840bb1b..000000000 --- a/providers/dns/corenetworks/corenetworks.toml +++ /dev/null @@ -1,25 +0,0 @@ -Name = "Core-Networks" -Description = '''''' -URL = "https://www.core-networks.de/" -Code = "corenetworks" -Since = "v4.20.0" - -Example = ''' -CORENETWORKS_LOGIN="xxxx" \ -CORENETWORKS_PASSWORD="yyyy" \ -lego --dns corenetworks -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - CORENETWORKS_LOGIN = "The username of the API account" - CORENETWORKS_PASSWORD = "The password" - [Configuration.Additional] - CORENETWORKS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - CORENETWORKS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - CORENETWORKS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)" - CORENETWORKS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - CORENETWORKS_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 60)" - -[Links] - API = "https://beta.api.core-networks.de/doc/" diff --git a/providers/dns/corenetworks/corenetworks_test.go b/providers/dns/corenetworks/corenetworks_test.go deleted file mode 100644 index 911693468..000000000 --- a/providers/dns/corenetworks/corenetworks_test.go +++ /dev/null @@ -1,135 +0,0 @@ -package corenetworks - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvLogin, EnvPassword).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvLogin: "user", - EnvPassword: "secret", - }, - }, - { - desc: "missing login", - envVars: map[string]string{ - EnvPassword: "secret", - }, - expected: "corenetworks: some credentials information are missing: CORENETWORKS_LOGIN", - }, - { - desc: "missing password", - envVars: map[string]string{ - EnvLogin: "user", - }, - expected: "corenetworks: some credentials information are missing: CORENETWORKS_PASSWORD", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - login string - password string - expected string - }{ - { - desc: "success", - login: "user", - password: "secret", - }, - { - desc: "missing login", - password: "secret", - expected: "corenetworks: credentials missing", - }, - { - desc: "missing password", - login: "user", - expected: "corenetworks: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.Login = test.login - config.Password = test.password - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/corenetworks/internal/client.go b/providers/dns/corenetworks/internal/client.go deleted file mode 100644 index bdc17f2c1..000000000 --- a/providers/dns/corenetworks/internal/client.go +++ /dev/null @@ -1,217 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" -) - -const defaultBaseURL = "https://beta.api.core-networks.de" - -// Client a Core-Networks client. -type Client struct { - login string - password string - - baseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(login, password string) *Client { - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - login: login, - password: password, - baseURL: baseURL, - HTTPClient: &http.Client{Timeout: 5 * time.Second}, - } -} - -// ListZone gets a list of all DNS zones. -// https://beta.api.core-networks.de/doc/#functon_dnszones -func (c *Client) ListZone(ctx context.Context) ([]Zone, error) { - endpoint := c.baseURL.JoinPath("dnszones") - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var zones []Zone - - err = c.do(req, &zones) - if err != nil { - return nil, err - } - - return zones, nil -} - -// GetZoneDetails provides detailed information about a DNS zone. -// https://beta.api.core-networks.de/doc/#functon_dnszones_details -func (c *Client) GetZoneDetails(ctx context.Context, zone string) (*ZoneDetails, error) { - endpoint := c.baseURL.JoinPath("dnszones", zone) - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var details ZoneDetails - - err = c.do(req, &details) - if err != nil { - return nil, err - } - - return &details, nil -} - -// ListRecords gets a list of DNS records belonging to the zone. -// https://beta.api.core-networks.de/doc/#functon_dnszones_records -func (c *Client) ListRecords(ctx context.Context, zone string) ([]Record, error) { - endpoint := c.baseURL.JoinPath("dnszones", zone, "records") - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var records []Record - - err = c.do(req, &records) - if err != nil { - return nil, err - } - - return records, nil -} - -// AddRecord adds a record. -// https://beta.api.core-networks.de/doc/#functon_dnszones_records_add -func (c *Client) AddRecord(ctx context.Context, zone string, record Record) error { - endpoint := c.baseURL.JoinPath("dnszones", zone, "records", "/") - - if record.Name == "" { - record.Name = "@" - } - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) - if err != nil { - return err - } - - err = c.do(req, nil) - if err != nil { - return err - } - - return nil -} - -// DeleteRecords deletes all DNS records of a zone that match the DNS record passed. -// https://beta.api.core-networks.de/doc/#functon_dnszones_records_delete -func (c *Client) DeleteRecords(ctx context.Context, zone string, record Record) error { - endpoint := c.baseURL.JoinPath("dnszones", zone, "records", "delete") - - if record.Name == "" { - record.Name = "@" - } - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) - if err != nil { - return err - } - - err = c.do(req, nil) - if err != nil { - return err - } - - return nil -} - -// CommitRecords sends a commit to the zone. -// https://beta.api.core-networks.de/doc/#functon_dnszones_commit -func (c *Client) CommitRecords(ctx context.Context, zone string) error { - endpoint := c.baseURL.JoinPath("dnszones", zone, "records", "commit") - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, nil) - if err != nil { - return err - } - - err = c.do(req, nil) - if err != nil { - return err - } - - return nil -} - -func (c *Client) do(req *http.Request, result any) error { - at := getToken(req.Context()) - if at != "" { - req.Header.Set(authorizationHeader, "Bearer "+at) - } - - resp, errD := c.HTTPClient.Do(req) - if errD != nil { - return errutils.NewHTTPDoError(req, errD) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return errutils.NewUnexpectedResponseStatusCodeError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} diff --git a/providers/dns/corenetworks/internal/client_test.go b/providers/dns/corenetworks/internal/client_test.go deleted file mode 100644 index ca5c81a65..000000000 --- a/providers/dns/corenetworks/internal/client_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("user", "secret") - client.baseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(), - ) -} - -func TestClient_ListZone(t *testing.T) { - client := mockBuilder(). - Route("GET /dnszones/", - servermock.ResponseFromFixture("ListZone.json")). - Build(t) - - ctx := t.Context() - - zones, err := client.ListZone(ctx) - require.NoError(t, err) - - expected := []Zone{ - {Name: "example.com", Type: "master"}, - {Name: "example.net", Type: "slave"}, - } - - assert.Equal(t, expected, zones) -} - -func TestClient_GetZoneDetails(t *testing.T) { - client := mockBuilder(). - Route("GET /dnszones/example.com", - servermock.ResponseFromFixture("GetZoneDetails.json")). - Build(t) - - zone, err := client.GetZoneDetails(t.Context(), "example.com") - require.NoError(t, err) - - expected := &ZoneDetails{ - Active: true, - DNSSec: true, - Name: "example.com", - Type: "master", - } - - assert.Equal(t, expected, zone) -} - -func TestClient_ListRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /dnszones/example.com/records/", - servermock.ResponseFromFixture("ListRecords.json")). - Build(t) - - records, err := client.ListRecords(t.Context(), "example.com") - require.NoError(t, err) - - expected := []Record{ - { - Name: "@", - TTL: 86400, - Type: "NS", - Data: "ns2.core-networks.eu.", - }, - { - Name: "@", - TTL: 86400, - Type: "NS", - Data: "ns3.core-networks.com.", - }, - { - Name: "@", - TTL: 86400, - Type: "NS", - Data: "ns1.core-networks.de.", - }, - } - - assert.Equal(t, expected, records) -} - -func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /dnszones/example.com/records/", - servermock.Noop().WithStatusCode(http.StatusNoContent)). - Build(t) - - record := Record{Name: "www", TTL: 3600, Type: "A", Data: "127.0.0.1"} - - err := client.AddRecord(t.Context(), "example.com", record) - require.NoError(t, err) -} - -func TestClient_DeleteRecords(t *testing.T) { - client := mockBuilder(). - Route("POST /dnszones/example.com/records/delete", - servermock.Noop().WithStatusCode(http.StatusNoContent)). - Build(t) - - record := Record{Name: "www", Type: "A", Data: "127.0.0.1"} - - err := client.DeleteRecords(t.Context(), "example.com", record) - require.NoError(t, err) -} - -func TestClient_CommitRecords(t *testing.T) { - client := mockBuilder(). - Route("POST /dnszones/example.com/records/commit", - servermock.Noop().WithStatusCode(http.StatusNoContent)). - Build(t) - - err := client.CommitRecords(t.Context(), "example.com") - require.NoError(t, err) -} diff --git a/providers/dns/corenetworks/internal/fixtures/GetZoneDetails.json b/providers/dns/corenetworks/internal/fixtures/GetZoneDetails.json deleted file mode 100644 index 1fce07238..000000000 --- a/providers/dns/corenetworks/internal/fixtures/GetZoneDetails.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "active": true, - "dnssec": true, - "master": null, - "name": "example.com", - "tsig": null, - "type": "master" -} diff --git a/providers/dns/corenetworks/internal/fixtures/ListRecords.json b/providers/dns/corenetworks/internal/fixtures/ListRecords.json deleted file mode 100644 index a09ff9c1a..000000000 --- a/providers/dns/corenetworks/internal/fixtures/ListRecords.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "name": "@", - "ttl": 86400, - "type": "NS", - "data": "ns2.core-networks.eu." - }, - { - "name": "@", - "ttl": 86400, - "type": "NS", - "data": "ns3.core-networks.com." - }, - { - "name": "@", - "ttl": 86400, - "type": "NS", - "data": "ns1.core-networks.de." - } -] diff --git a/providers/dns/corenetworks/internal/fixtures/ListZone.json b/providers/dns/corenetworks/internal/fixtures/ListZone.json deleted file mode 100644 index 726380873..000000000 --- a/providers/dns/corenetworks/internal/fixtures/ListZone.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "name": "example.com", - "type": "master" - }, - { - "name": "example.net", - "type": "slave" - } -] diff --git a/providers/dns/corenetworks/internal/fixtures/auth.json b/providers/dns/corenetworks/internal/fixtures/auth.json deleted file mode 100644 index 399a18007..000000000 --- a/providers/dns/corenetworks/internal/fixtures/auth.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "token": "authsecret", - "expires": 123 -} diff --git a/providers/dns/corenetworks/internal/identity.go b/providers/dns/corenetworks/internal/identity.go deleted file mode 100644 index a7e7448c0..000000000 --- a/providers/dns/corenetworks/internal/identity.go +++ /dev/null @@ -1,50 +0,0 @@ -package internal - -import ( - "context" - "net/http" -) - -const authorizationHeader = "Authorization" - -type token string - -const tokenKey token = "token" - -// CreateAuthenticationToken gets an authentication token. -// https://beta.api.core-networks.de/doc/#functon_auth_token -func (c *Client) CreateAuthenticationToken(ctx context.Context) (*Token, error) { - endpoint := c.baseURL.JoinPath("auth", "token") - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, Auth{Login: c.login, Password: c.password}) - if err != nil { - return nil, err - } - - var token Token - - err = c.do(req, &token) - if err != nil { - return nil, err - } - - return &token, nil -} - -func (c *Client) CreateAuthenticatedContext(ctx context.Context) (context.Context, error) { - tok, err := c.CreateAuthenticationToken(ctx) - if err != nil { - return nil, err - } - - return context.WithValue(ctx, tokenKey, tok.Token), nil -} - -func getToken(ctx context.Context) string { - tok, ok := ctx.Value(tokenKey).(string) - if !ok { - return "" - } - - return tok -} diff --git a/providers/dns/corenetworks/internal/identity_test.go b/providers/dns/corenetworks/internal/identity_test.go deleted file mode 100644 index b5e05ed3f..000000000 --- a/providers/dns/corenetworks/internal/identity_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package internal - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestClient_CreateAuthenticationToken(t *testing.T) { - client := mockBuilder(). - Route("POST /auth/token", servermock.ResponseFromFixture("auth.json")). - Build(t) - - token, err := client.CreateAuthenticationToken(t.Context()) - require.NoError(t, err) - - expected := &Token{ - Token: "authsecret", - Expires: 123, - } - assert.Equal(t, expected, token) -} diff --git a/providers/dns/corenetworks/internal/types.go b/providers/dns/corenetworks/internal/types.go deleted file mode 100644 index 77b0378c2..000000000 --- a/providers/dns/corenetworks/internal/types.go +++ /dev/null @@ -1,37 +0,0 @@ -package internal - -type Auth struct { - Login string `json:"login,omitempty"` - Password string `json:"password,omitempty"` -} - -type Token struct { - Token string `json:"token,omitempty"` - Expires int `json:"expires,omitempty"` -} - -type Zone struct { - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` -} - -type ZoneDetails struct { - Active bool `json:"active,omitempty"` - DNSSec bool `json:"dnssec,omitempty"` - Master string `json:"master,omitempty"` - Name string `json:"name,omitempty"` - TSIG *TSIGKey `json:"tsig,omitempty"` - Type string `json:"type,omitempty"` -} - -type TSIGKey struct { - Algo string `json:"algo,omitempty"` - Secret string `json:"secret,omitempty"` -} - -type Record struct { - Name string `json:"name,omitempty"` - TTL int `json:"ttl,omitempty"` - Type string `json:"type,omitempty"` - Data string `json:"data,omitempty"` -} diff --git a/providers/dns/cpanel/cpanel.go b/providers/dns/cpanel/cpanel.go index f335c0a8c..bb025c2a3 100644 --- a/providers/dns/cpanel/cpanel.go +++ b/providers/dns/cpanel/cpanel.go @@ -11,13 +11,11 @@ import ( "strings" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/cpanel/internal/cpanel" "github.com/go-acme/lego/v4/providers/dns/cpanel/internal/shared" "github.com/go-acme/lego/v4/providers/dns/cpanel/internal/whm" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -35,8 +33,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - type apiClient interface { FetchZoneInformation(ctx context.Context, domain string) ([]shared.ZoneRecord, error) AddRecord(ctx context.Context, serial uint32, domain string, record shared.Record) (*shared.ZoneSerial, error) @@ -147,16 +143,12 @@ func (d *DNSProvider) Present(domain, _, keyAuth string) error { valueB64 := base64.StdEncoding.EncodeToString([]byte(info.Value)) - var ( - found bool - existingRecord shared.ZoneRecord - ) - + var found bool + var existingRecord shared.ZoneRecord for _, record := range zoneInfo { if slices.Contains(record.DataB64, valueB64) { existingRecord = record found = true - break } } @@ -225,16 +217,12 @@ func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { valueB64 := base64.StdEncoding.EncodeToString([]byte(info.Value)) - var ( - found bool - existingRecord shared.ZoneRecord - ) - + var found bool + var existingRecord shared.ZoneRecord for _, record := range zoneInfo { if slices.Contains(record.DataB64, valueB64) { existingRecord = record found = true - break } } @@ -244,7 +232,6 @@ func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { } var newData []string - for _, dataB64 := range existingRecord.DataB64 { if dataB64 == valueB64 { continue @@ -301,7 +288,6 @@ func getZoneSerial(zoneFqdn string, zoneInfo []shared.ZoneRecord) (uint32, error } var newSerial uint32 - _, err = fmt.Sscan(string(data), &newSerial) if err != nil { return 0, fmt.Errorf("decode serial DNameB64, invalid serial value %q: %w", string(data), err) @@ -325,8 +311,6 @@ func createClient(config *Config) (apiClient, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return client, nil case "whm": @@ -339,8 +323,6 @@ func createClient(config *Config) (apiClient, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return client, nil default: diff --git a/providers/dns/cpanel/cpanel.toml b/providers/dns/cpanel/cpanel.toml index b64adf0cf..eac811eff 100644 --- a/providers/dns/cpanel/cpanel.toml +++ b/providers/dns/cpanel/cpanel.toml @@ -7,18 +7,18 @@ Since = "v4.16.0" Example = ''' ### CPANEL (default) -CPANEL_USERNAME="yyyy" \ -CPANEL_TOKEN="xxxx" \ -CPANEL_BASE_URL="https://example.com:2083" \ -lego --dns cpanel -d '*.example.com' -d example.com run +CPANEL_USERNAME = "yyyy" +CPANEL_TOKEN = "xxxx" +CPANEL_BASE_URL = "https://example.com:2083" \ +lego --email you@example.com --dns cpanel --domains my.example.org run ## WHM -CPANEL_MODE=whm \ -CPANEL_USERNAME="yyyy" \ -CPANEL_TOKEN="xxxx" \ -CPANEL_BASE_URL="https://example.com:2087" \ -lego --dns cpanel -d '*.example.com' -d example.com run +CPANEL_MODE = whm +CPANEL_USERNAME = "yyyy" +CPANEL_TOKEN = "xxxx" +CPANEL_BASE_URL = "https://example.com:2087" \ +lego --email you@example.com --dns cpanel --domains my.example.org run ''' [Configuration] @@ -28,10 +28,11 @@ lego --dns cpanel -d '*.example.com' -d example.com run CPANEL_BASE_URL = "API server URL" [Configuration.Additional] CPANEL_MODE = "use cpanel API or WHM API (Default: cpanel)" - CPANEL_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - CPANEL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - CPANEL_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - CPANEL_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + CPANEL_POLLING_INTERVAL = "Time between DNS propagation check" + CPANEL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + CPANEL_TTL = "The TTL of the TXT record used for the DNS challenge" + CPANEL_HTTP_TIMEOUT = "API request timeout" + CPANEL_REGION = "The region" [Links] API_CPANEL = "https://api.docs.cpanel.net/cpanel/introduction/" diff --git a/providers/dns/cpanel/cpanel_test.go b/providers/dns/cpanel/cpanel_test.go index 5d85b8b5b..614b9e1c7 100644 --- a/providers/dns/cpanel/cpanel_test.go +++ b/providers/dns/cpanel/cpanel_test.go @@ -75,7 +75,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -283,7 +282,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -297,7 +295,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/cpanel/internal/cpanel/client.go b/providers/dns/cpanel/internal/cpanel/client.go index e869f6f4b..3bca6b521 100644 --- a/providers/dns/cpanel/internal/cpanel/client.go +++ b/providers/dns/cpanel/internal/cpanel/client.go @@ -24,7 +24,7 @@ type Client struct { HTTPClient *http.Client } -func NewClient(baseURL, username, token string) (*Client, error) { +func NewClient(baseURL string, username string, token string) (*Client, error) { apiEndpoint, err := url.Parse(baseURL) if err != nil { return nil, err @@ -40,7 +40,7 @@ func NewClient(baseURL, username, token string) (*Client, error) { // FetchZoneInformation fetches zone information. // https://api.docs.cpanel.net/openapi/cpanel/operation/dns-parse_zone/ -func (c *Client) FetchZoneInformation(ctx context.Context, domain string) ([]shared.ZoneRecord, error) { +func (c Client) FetchZoneInformation(ctx context.Context, domain string) ([]shared.ZoneRecord, error) { endpoint := c.baseURL.JoinPath("DNS", "parse_zone") query := endpoint.Query() @@ -64,7 +64,7 @@ func (c *Client) FetchZoneInformation(ctx context.Context, domain string) ([]sha // AddRecord adds a new record. // // add='{"dname":"example", "ttl":14400, "record_type":"TXT", "data":["string1", "string2"]}' -func (c *Client) AddRecord(ctx context.Context, serial uint32, domain string, record shared.Record) (*shared.ZoneSerial, error) { +func (c Client) AddRecord(ctx context.Context, serial uint32, domain string, record shared.Record) (*shared.ZoneSerial, error) { data, err := json.Marshal(record) if err != nil { return nil, fmt.Errorf("failed to create request JSON data: %w", err) @@ -76,7 +76,7 @@ func (c *Client) AddRecord(ctx context.Context, serial uint32, domain string, re // EditRecord edits an existing record. // // edit='{"line_index": 9, "dname":"example", "ttl":14400, "record_type":"TXT", "data":["string1", "string2"]}' -func (c *Client) EditRecord(ctx context.Context, serial uint32, domain string, record shared.Record) (*shared.ZoneSerial, error) { +func (c Client) EditRecord(ctx context.Context, serial uint32, domain string, record shared.Record) (*shared.ZoneSerial, error) { data, err := json.Marshal(record) if err != nil { return nil, fmt.Errorf("failed to create request JSON data: %w", err) @@ -88,12 +88,12 @@ func (c *Client) EditRecord(ctx context.Context, serial uint32, domain string, r // DeleteRecord deletes an existing record. // // remove=22 -func (c *Client) DeleteRecord(ctx context.Context, serial uint32, domain string, lineIndex int) (*shared.ZoneSerial, error) { +func (c Client) DeleteRecord(ctx context.Context, serial uint32, domain string, lineIndex int) (*shared.ZoneSerial, error) { return c.updateZone(ctx, serial, domain, "remove", strconv.Itoa(lineIndex)) } // https://api.docs.cpanel.net/openapi/cpanel/operation/dns-mass_edit_zone/ -func (c *Client) updateZone(ctx context.Context, serial uint32, domain, action, data string) (*shared.ZoneSerial, error) { +func (c Client) updateZone(ctx context.Context, serial uint32, domain, action, data string) (*shared.ZoneSerial, error) { endpoint := c.baseURL.JoinPath("DNS", "mass_edit_zone") query := endpoint.Query() @@ -116,7 +116,7 @@ func (c *Client) updateZone(ctx context.Context, serial uint32, domain, action, return &result.Data, nil } -func (c *Client) doRequest(ctx context.Context, endpoint *url.URL, result any) error { +func (c Client) doRequest(ctx context.Context, endpoint *url.URL, result any) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody) if err != nil { return fmt.Errorf("unable to create request: %w", err) diff --git a/providers/dns/cpanel/internal/cpanel/client_test.go b/providers/dns/cpanel/internal/cpanel/client_test.go index 533d1130d..8516259d6 100644 --- a/providers/dns/cpanel/internal/cpanel/client_test.go +++ b/providers/dns/cpanel/internal/cpanel/client_test.go @@ -1,40 +1,61 @@ package cpanel import ( + "context" + "fmt" + "io" + "net/http" "net/http/httptest" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/go-acme/lego/v4/providers/dns/cpanel/internal/shared" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient(server.URL, "user", "secret") - if err != nil { - return nil, err - } +func setupTest(t *testing.T, pattern string, filename string) *Client { + t.Helper() - client.HTTPClient = server.Client() + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("cpanel user:secret")) + mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + open, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = open.Close() }() + + rw.WriteHeader(http.StatusOK) + _, err = io.Copy(rw, open) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + client, err := NewClient(server.URL, "user", "secret") + require.NoError(t, err) + + client.HTTPClient = server.Client() + + return client } func TestClient_FetchZoneInformation(t *testing.T) { - client := mockBuilder(). - Route("GET /execute/DNS/parse_zone", - servermock.ResponseFromFixture("zone-info.json"), - servermock.CheckQueryParameter().Strict(). - With("zone", "example.com")). - Build(t) + client := setupTest(t, "/execute/DNS/parse_zone", "zone-info.json") - zoneInfo, err := client.FetchZoneInformation(t.Context(), "example.com") + zoneInfo, err := client.FetchZoneInformation(context.Background(), "example.com") require.NoError(t, err) expected := []shared.ZoneRecord{{ @@ -50,27 +71,16 @@ func TestClient_FetchZoneInformation(t *testing.T) { } func TestClient_FetchZoneInformation_error(t *testing.T) { - client := mockBuilder(). - Route("GET /execute/DNS/parse_zone", - servermock.ResponseFromFixture("zone-info_error.json")). - Build(t) + client := setupTest(t, "/execute/DNS/parse_zone", "zone-info_error.json") - zoneInfo, err := client.FetchZoneInformation(t.Context(), "example.com") - require.EqualError(t, err, "error(0): You do not control a DNS zone named example.com.: a, b, c") + zoneInfo, err := client.FetchZoneInformation(context.Background(), "example.com") + require.Error(t, err) assert.Nil(t, zoneInfo) } func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("GET /execute/DNS/mass_edit_zone", - servermock.ResponseFromFixture("update-zone.json"), - servermock.CheckQueryParameter().Strict(). - With("zone", "example.com"). - With("add", `{"dname":"example","ttl":14400,"record_type":"TXT","data":["string1","string2"]}`). - With("serial", "123456"). - With("zone", "example.com")). - Build(t) + client := setupTest(t, "/execute/DNS/mass_edit_zone", "update-zone.json") record := shared.Record{ DName: "example", @@ -79,7 +89,7 @@ func TestClient_AddRecord(t *testing.T) { Data: []string{"string1", "string2"}, } - zoneSerial, err := client.AddRecord(t.Context(), 123456, "example.com", record) + zoneSerial, err := client.AddRecord(context.Background(), 123456, "example.com", record) require.NoError(t, err) expected := &shared.ZoneSerial{NewSerial: "2021031903"} @@ -88,10 +98,7 @@ func TestClient_AddRecord(t *testing.T) { } func TestClient_AddRecord_error(t *testing.T) { - client := mockBuilder(). - Route("GET /execute/DNS/mass_edit_zone", - servermock.ResponseFromFixture("update-zone_error.json")). - Build(t) + client := setupTest(t, "/execute/DNS/mass_edit_zone", "update-zone_error.json") record := shared.Record{ DName: "example", @@ -100,21 +107,14 @@ func TestClient_AddRecord_error(t *testing.T) { Data: []string{"string1", "string2"}, } - zoneSerial, err := client.AddRecord(t.Context(), 123456, "example.com", record) + zoneSerial, err := client.AddRecord(context.Background(), 123456, "example.com", record) require.Error(t, err) assert.Nil(t, zoneSerial) } func TestClient_EditRecord(t *testing.T) { - client := mockBuilder(). - Route("GET /execute/DNS/mass_edit_zone", - servermock.ResponseFromFixture("update-zone.json"), - servermock.CheckQueryParameter().Strict(). - With("edit", `{"dname":"example","ttl":14400,"record_type":"TXT","data":["string1","string2"],"line_index":9}`). - With("serial", "123456"). - With("zone", "example.com")). - Build(t) + client := setupTest(t, "/execute/DNS/mass_edit_zone", "update-zone.json") record := shared.Record{ LineIndex: 9, @@ -124,7 +124,7 @@ func TestClient_EditRecord(t *testing.T) { Data: []string{"string1", "string2"}, } - zoneSerial, err := client.EditRecord(t.Context(), 123456, "example.com", record) + zoneSerial, err := client.EditRecord(context.Background(), 123456, "example.com", record) require.NoError(t, err) expected := &shared.ZoneSerial{NewSerial: "2021031903"} @@ -133,10 +133,7 @@ func TestClient_EditRecord(t *testing.T) { } func TestClient_EditRecord_error(t *testing.T) { - client := mockBuilder(). - Route("GET /execute/DNS/mass_edit_zone", - servermock.ResponseFromFixture("update-zone_error.json")). - Build(t) + client := setupTest(t, "/execute/DNS/mass_edit_zone", "update-zone_error.json") record := shared.Record{ LineIndex: 9, @@ -146,23 +143,16 @@ func TestClient_EditRecord_error(t *testing.T) { Data: []string{"string1", "string2"}, } - zoneSerial, err := client.EditRecord(t.Context(), 123456, "example.com", record) + zoneSerial, err := client.EditRecord(context.Background(), 123456, "example.com", record) require.Error(t, err) assert.Nil(t, zoneSerial) } func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("GET /execute/DNS/mass_edit_zone", - servermock.ResponseFromFixture("update-zone.json"), - servermock.CheckQueryParameter().Strict(). - With("remove", "0"). - With("serial", "123456"). - With("zone", "example.com")). - Build(t) + client := setupTest(t, "/execute/DNS/mass_edit_zone", "update-zone.json") - zoneSerial, err := client.DeleteRecord(t.Context(), 123456, "example.com", 0) + zoneSerial, err := client.DeleteRecord(context.Background(), 123456, "example.com", 0) require.NoError(t, err) expected := &shared.ZoneSerial{NewSerial: "2021031903"} @@ -171,12 +161,9 @@ func TestClient_DeleteRecord(t *testing.T) { } func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("GET /execute/DNS/mass_edit_zone", - servermock.ResponseFromFixture("update-zone_error.json")). - Build(t) + client := setupTest(t, "/execute/DNS/mass_edit_zone", "update-zone_error.json") - zoneSerial, err := client.DeleteRecord(t.Context(), 123456, "example.com", 0) + zoneSerial, err := client.DeleteRecord(context.Background(), 123456, "example.com", 0) require.Error(t, err) assert.Nil(t, zoneSerial) diff --git a/providers/dns/cpanel/internal/cpanel/types.go b/providers/dns/cpanel/internal/cpanel/types.go index 0a3053647..cb4dbd535 100644 --- a/providers/dns/cpanel/internal/cpanel/types.go +++ b/providers/dns/cpanel/internal/cpanel/types.go @@ -6,7 +6,7 @@ import ( ) type APIResponse[T any] struct { - Metadata Metadata `json:"metadata"` + Metadata Metadata `json:"metadata,omitempty"` Data T `json:"data,omitempty"` Status int `json:"status,omitempty"` diff --git a/providers/dns/cpanel/internal/whm/client.go b/providers/dns/cpanel/internal/whm/client.go index 742b25b6a..d375b83e3 100644 --- a/providers/dns/cpanel/internal/whm/client.go +++ b/providers/dns/cpanel/internal/whm/client.go @@ -24,7 +24,7 @@ type Client struct { HTTPClient *http.Client } -func NewClient(baseURL, username, token string) (*Client, error) { +func NewClient(baseURL string, username string, token string) (*Client, error) { apiEndpoint, err := url.Parse(baseURL) if err != nil { return nil, err @@ -40,7 +40,7 @@ func NewClient(baseURL, username, token string) (*Client, error) { // FetchZoneInformation fetches zone information. // https://api.docs.cpanel.net/openapi/whm/operation/parse_dns_zone/ -func (c *Client) FetchZoneInformation(ctx context.Context, domain string) ([]shared.ZoneRecord, error) { +func (c Client) FetchZoneInformation(ctx context.Context, domain string) ([]shared.ZoneRecord, error) { endpoint := c.baseURL.JoinPath("parse_dns_zone") query := endpoint.Query() @@ -64,7 +64,7 @@ func (c *Client) FetchZoneInformation(ctx context.Context, domain string) ([]sha // AddRecord adds a new record. // // add='{"dname":"example", "ttl":14400, "record_type":"TXT", "data":["string1", "string2"]}' -func (c *Client) AddRecord(ctx context.Context, serial uint32, domain string, record shared.Record) (*shared.ZoneSerial, error) { +func (c Client) AddRecord(ctx context.Context, serial uint32, domain string, record shared.Record) (*shared.ZoneSerial, error) { data, err := json.Marshal(record) if err != nil { return nil, fmt.Errorf("failed to create request JSON data: %w", err) @@ -76,7 +76,7 @@ func (c *Client) AddRecord(ctx context.Context, serial uint32, domain string, re // EditRecord edits an existing record. // // edit='{"line_index": 9, "dname":"example", "ttl":14400, "record_type":"TXT", "data":["string1", "string2"]}' -func (c *Client) EditRecord(ctx context.Context, serial uint32, domain string, record shared.Record) (*shared.ZoneSerial, error) { +func (c Client) EditRecord(ctx context.Context, serial uint32, domain string, record shared.Record) (*shared.ZoneSerial, error) { data, err := json.Marshal(record) if err != nil { return nil, fmt.Errorf("failed to create request JSON data: %w", err) @@ -88,12 +88,12 @@ func (c *Client) EditRecord(ctx context.Context, serial uint32, domain string, r // DeleteRecord deletes an existing record. // // remove=22 -func (c *Client) DeleteRecord(ctx context.Context, serial uint32, domain string, lineIndex int) (*shared.ZoneSerial, error) { +func (c Client) DeleteRecord(ctx context.Context, serial uint32, domain string, lineIndex int) (*shared.ZoneSerial, error) { return c.updateZone(ctx, serial, domain, "remove", strconv.Itoa(lineIndex)) } // https://api.docs.cpanel.net/openapi/whm/operation/mass_edit_dns_zone/ -func (c *Client) updateZone(ctx context.Context, serial uint32, domain, action, data string) (*shared.ZoneSerial, error) { +func (c Client) updateZone(ctx context.Context, serial uint32, domain, action, data string) (*shared.ZoneSerial, error) { endpoint := c.baseURL.JoinPath("mass_edit_dns_zone") query := endpoint.Query() @@ -116,7 +116,7 @@ func (c *Client) updateZone(ctx context.Context, serial uint32, domain, action, return &result.Data, nil } -func (c *Client) doRequest(ctx context.Context, endpoint *url.URL, result any) error { +func (c Client) doRequest(ctx context.Context, endpoint *url.URL, result any) error { query := endpoint.Query() query.Set("api.version", "1") endpoint.RawQuery = query.Encode() diff --git a/providers/dns/cpanel/internal/whm/client_test.go b/providers/dns/cpanel/internal/whm/client_test.go index 47686bf09..f4f6d7b19 100644 --- a/providers/dns/cpanel/internal/whm/client_test.go +++ b/providers/dns/cpanel/internal/whm/client_test.go @@ -1,41 +1,61 @@ package whm import ( + "context" + "fmt" + "io" + "net/http" "net/http/httptest" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/go-acme/lego/v4/providers/dns/cpanel/internal/shared" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient(server.URL, "user", "secret") - if err != nil { - return nil, err - } +func setupTest(t *testing.T, pattern string, filename string) *Client { + t.Helper() - client.HTTPClient = server.Client() + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("whm user:secret")) + mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + open, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = open.Close() }() + + rw.WriteHeader(http.StatusOK) + _, err = io.Copy(rw, open) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + client, err := NewClient(server.URL, "user", "secret") + require.NoError(t, err) + + client.HTTPClient = server.Client() + + return client } func TestClient_FetchZoneInformation(t *testing.T) { - client := mockBuilder(). - Route("GET /json-api/parse_dns_zone", - servermock.ResponseFromFixture("zone-info.json"), - servermock.CheckQueryParameter().Strict(). - With("api.version", "1"). - With("zone", "example.com")). - Build(t) + client := setupTest(t, "/json-api/parse_dns_zone", "zone-info.json") - zoneInfo, err := client.FetchZoneInformation(t.Context(), "example.com") + zoneInfo, err := client.FetchZoneInformation(context.Background(), "example.com") require.NoError(t, err) expected := []shared.ZoneRecord{{ @@ -51,27 +71,16 @@ func TestClient_FetchZoneInformation(t *testing.T) { } func TestClient_FetchZoneInformation_error(t *testing.T) { - client := mockBuilder(). - Route("GET /json-api/parse_dns_zone", - servermock.ResponseFromFixture("zone-info_error.json")). - Build(t) + client := setupTest(t, "/json-api/parse_dns_zone", "zone-info_error.json") - zoneInfo, err := client.FetchZoneInformation(t.Context(), "example.com") + zoneInfo, err := client.FetchZoneInformation(context.Background(), "example.com") require.Error(t, err) assert.Nil(t, zoneInfo) } func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("GET /json-api/mass_edit_dns_zone", - servermock.ResponseFromFixture("update-zone.json"), - servermock.CheckQueryParameter().Strict(). - With("add", `{"dname":"example","ttl":14400,"record_type":"TXT","data":["string1","string2"]}`). - With("api.version", "1"). - With("serial", "123456"). - With("zone", "example.com")). - Build(t) + client := setupTest(t, "/json-api/mass_edit_dns_zone", "update-zone.json") record := shared.Record{ DName: "example", @@ -80,7 +89,7 @@ func TestClient_AddRecord(t *testing.T) { Data: []string{"string1", "string2"}, } - zoneSerial, err := client.AddRecord(t.Context(), 123456, "example.com", record) + zoneSerial, err := client.AddRecord(context.Background(), 123456, "example.com", record) require.NoError(t, err) expected := &shared.ZoneSerial{NewSerial: "2021031903"} @@ -89,10 +98,7 @@ func TestClient_AddRecord(t *testing.T) { } func TestClient_AddRecord_error(t *testing.T) { - client := mockBuilder(). - Route("GET /json-api/mass_edit_dns_zone", - servermock.ResponseFromFixture("update-zone_error.json")). - Build(t) + client := setupTest(t, "/json-api/mass_edit_dns_zone", "update-zone_error.json") record := shared.Record{ DName: "example", @@ -101,22 +107,14 @@ func TestClient_AddRecord_error(t *testing.T) { Data: []string{"string1", "string2"}, } - zoneSerial, err := client.AddRecord(t.Context(), 123456, "example.com", record) + zoneSerial, err := client.AddRecord(context.Background(), 123456, "example.com", record) require.Error(t, err) assert.Nil(t, zoneSerial) } func TestClient_EditRecord(t *testing.T) { - client := mockBuilder(). - Route("GET /json-api/mass_edit_dns_zone", - servermock.ResponseFromFixture("update-zone.json"), - servermock.CheckQueryParameter().Strict(). - With("edit", `{"dname":"example","ttl":14400,"record_type":"TXT","data":["string1","string2"],"line_index":9}`). - With("api.version", "1"). - With("serial", "123456"). - With("zone", "example.com")). - Build(t) + client := setupTest(t, "/json-api/mass_edit_dns_zone", "update-zone.json") record := shared.Record{ LineIndex: 9, @@ -126,7 +124,7 @@ func TestClient_EditRecord(t *testing.T) { Data: []string{"string1", "string2"}, } - zoneSerial, err := client.EditRecord(t.Context(), 123456, "example.com", record) + zoneSerial, err := client.EditRecord(context.Background(), 123456, "example.com", record) require.NoError(t, err) expected := &shared.ZoneSerial{NewSerial: "2021031903"} @@ -135,10 +133,7 @@ func TestClient_EditRecord(t *testing.T) { } func TestClient_EditRecord_error(t *testing.T) { - client := mockBuilder(). - Route("GET /json-api/mass_edit_dns_zone", - servermock.ResponseFromFixture("update-zone_error.json")). - Build(t) + client := setupTest(t, "/json-api/mass_edit_dns_zone", "update-zone_error.json") record := shared.Record{ LineIndex: 9, @@ -148,24 +143,16 @@ func TestClient_EditRecord_error(t *testing.T) { Data: []string{"string1", "string2"}, } - zoneSerial, err := client.EditRecord(t.Context(), 123456, "example.com", record) + zoneSerial, err := client.EditRecord(context.Background(), 123456, "example.com", record) require.Error(t, err) assert.Nil(t, zoneSerial) } func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("GET /json-api/mass_edit_dns_zone", - servermock.ResponseFromFixture("update-zone.json"), - servermock.CheckQueryParameter().Strict(). - With("remove", "0"). - With("api.version", "1"). - With("serial", "123456"). - With("zone", "example.com")). - Build(t) + client := setupTest(t, "/json-api/mass_edit_dns_zone", "update-zone.json") - zoneSerial, err := client.DeleteRecord(t.Context(), 123456, "example.com", 0) + zoneSerial, err := client.DeleteRecord(context.Background(), 123456, "example.com", 0) require.NoError(t, err) expected := &shared.ZoneSerial{NewSerial: "2021031903"} @@ -174,12 +161,9 @@ func TestClient_DeleteRecord(t *testing.T) { } func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("GET /json-api/mass_edit_dns_zone", - servermock.ResponseFromFixture("update-zone_error.json")). - Build(t) + client := setupTest(t, "/json-api/mass_edit_dns_zone", "update-zone_error.json") - zoneSerial, err := client.DeleteRecord(t.Context(), 123456, "example.com", 0) + zoneSerial, err := client.DeleteRecord(context.Background(), 123456, "example.com", 0) require.Error(t, err) assert.Nil(t, zoneSerial) diff --git a/providers/dns/cpanel/internal/whm/types.go b/providers/dns/cpanel/internal/whm/types.go index d0604a565..f1884a04d 100644 --- a/providers/dns/cpanel/internal/whm/types.go +++ b/providers/dns/cpanel/internal/whm/types.go @@ -7,7 +7,7 @@ import ( ) type APIResponse[T any] struct { - Metadata Metadata `json:"metadata"` + Metadata Metadata `json:"metadata,omitempty"` Data T `json:"data,omitempty"` } diff --git a/providers/dns/czechia/czechia.go b/providers/dns/czechia/czechia.go deleted file mode 100644 index 3ff397c35..000000000 --- a/providers/dns/czechia/czechia.go +++ /dev/null @@ -1,159 +0,0 @@ -// Package czechia implements a DNS provider for solving the DNS-01 challenge using Czechia. -package czechia - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/czechia/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "CZECHIA_" - - EnvToken = envNamespace + "TOKEN" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - Token string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for Czechia. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvToken) - if err != nil { - return nil, fmt.Errorf("czechia: %w", err) - } - - config := NewDefaultConfig() - config.Token = values[EnvToken] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Czechia. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("czechia: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.Token) - if err != nil { - return nil, fmt.Errorf("czechia: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("czechia: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("czechia: %w", err) - } - - record := internal.TXTRecord{ - Hostname: subDomain, - Text: info.Value, - TTL: d.config.TTL, - PublishZone: 1, - } - - err = d.client.AddTXTRecord(ctx, dns01.UnFqdn(authZone), record) - if err != nil { - return fmt.Errorf("czechia: add TXT record: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("czechia: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("czechia: %w", err) - } - - record := internal.TXTRecord{ - Hostname: subDomain, - Text: info.Value, - TTL: d.config.TTL, - PublishZone: 1, - } - - err = d.client.DeleteTXTRecord(ctx, dns01.UnFqdn(authZone), record) - if err != nil { - return fmt.Errorf("czechia: delete TXT record: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/czechia/czechia.toml b/providers/dns/czechia/czechia.toml deleted file mode 100644 index 2a66d2054..000000000 --- a/providers/dns/czechia/czechia.toml +++ /dev/null @@ -1,22 +0,0 @@ -Name = "Czechia" -Description = '''''' -URL = "https://www.czechia.com/" -Code = "czechia" -Since = "v4.33.0" - -Example = ''' -CZECHIA_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns czechia -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - CZECHIA_TOKEN = "Authorization token" - [Configuration.Additional] - CZECHIA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - CZECHIA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - CZECHIA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - CZECHIA_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://api.czechia.com/swagger/index.html" diff --git a/providers/dns/czechia/czechia_test.go b/providers/dns/czechia/czechia_test.go deleted file mode 100644 index 7d9a2676c..000000000 --- a/providers/dns/czechia/czechia_test.go +++ /dev/null @@ -1,165 +0,0 @@ -package czechia - -import ( - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvToken: "secret", - }, - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "czechia: some credentials information are missing: CZECHIA_TOKEN", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - token string - expected string - }{ - { - desc: "success", - token: "secret", - }, - { - desc: "missing credentials", - expected: "czechia: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.Token = test.token - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.Token = "secret" - config.HTTPClient = server.Client() - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - p.client.BaseURL, _ = url.Parse(server.URL) - - return p, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(). - With("AuthorizationToken", "secret"), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("POST /DNS/example.com/TXT", - servermock.Noop(), - servermock.CheckRequestJSONBodyFromInternal("add_txt_record-request.json"), - ). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("DELETE /DNS/example.com/TXT", - servermock.Noop(), - servermock.CheckRequestJSONBodyFromInternal("add_txt_record-request.json"), - ). - Build(t) - - err := provider.CleanUp("example.com", "abc", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/czechia/internal/client.go b/providers/dns/czechia/internal/client.go deleted file mode 100644 index f3e0e462e..000000000 --- a/providers/dns/czechia/internal/client.go +++ /dev/null @@ -1,124 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" -) - -const defaultBaseURL = "https://api.czechia.com/api" - -const authorizationTokenHeader = "AuthorizationToken" - -// Client the Czechia API client. -type Client struct { - token string - - BaseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(token string) (*Client, error) { - if token == "" { - return nil, errors.New("credentials missing") - } - - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - token: token, - BaseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -func (c *Client) AddTXTRecord(ctx context.Context, domain string, record TXTRecord) error { - endpoint := c.BaseURL.JoinPath("DNS", domain, "TXT") - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) - if err != nil { - return err - } - - return c.do(req, nil) -} - -func (c *Client) DeleteTXTRecord(ctx context.Context, domain string, record TXTRecord) error { - endpoint := c.BaseURL.JoinPath("DNS", domain, "TXT") - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, record) - if err != nil { - return err - } - - return c.do(req, nil) -} - -func (c *Client) do(req *http.Request, result any) error { - useragent.SetHeader(req.Header) - - req.Header.Set(authorizationTokenHeader, c.token) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - raw, _ := io.ReadAll(resp.Body) - - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} diff --git a/providers/dns/czechia/internal/client_test.go b/providers/dns/czechia/internal/client_test.go deleted file mode 100644 index c6f1141c5..000000000 --- a/providers/dns/czechia/internal/client_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package internal - -import ( - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("secret") - if err != nil { - return nil, err - } - - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(). - With(authorizationTokenHeader, "secret"), - ) -} - -func TestClient_AddTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /DNS/example.com/TXT", - servermock.Noop(), - servermock.CheckRequestJSONBodyFromFixture("add_txt_record-request.json"), - ). - Build(t) - - record := TXTRecord{ - Hostname: "_acme-challenge", - Text: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: 120, - PublishZone: 1, - } - - err := client.AddTXTRecord(t.Context(), "example.com", record) - require.NoError(t, err) -} - -func TestClient_DeleteTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /DNS/example.com/TXT", - servermock.Noop(), - servermock.CheckRequestJSONBodyFromFixture("add_txt_record-request.json"), - ). - Build(t) - - record := TXTRecord{ - Hostname: "_acme-challenge", - Text: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: 120, - PublishZone: 1, - } - - err := client.DeleteTXTRecord(t.Context(), "example.com", record) - require.NoError(t, err) -} diff --git a/providers/dns/czechia/internal/fixtures/add_txt_record-request.json b/providers/dns/czechia/internal/fixtures/add_txt_record-request.json deleted file mode 100644 index ed5830093..000000000 --- a/providers/dns/czechia/internal/fixtures/add_txt_record-request.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "hostName": "_acme-challenge", - "text": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "ttl": 120, - "publishZone": 1 -} diff --git a/providers/dns/czechia/internal/fixtures/delete_txt_record-request.json b/providers/dns/czechia/internal/fixtures/delete_txt_record-request.json deleted file mode 100644 index ed5830093..000000000 --- a/providers/dns/czechia/internal/fixtures/delete_txt_record-request.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "hostName": "_acme-challenge", - "text": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "ttl": 120, - "publishZone": 1 -} diff --git a/providers/dns/czechia/internal/types.go b/providers/dns/czechia/internal/types.go deleted file mode 100644 index f4a9bfef7..000000000 --- a/providers/dns/czechia/internal/types.go +++ /dev/null @@ -1,8 +0,0 @@ -package internal - -type TXTRecord struct { - Hostname string `json:"hostName,omitempty"` - Text string `json:"text,omitempty"` - TTL int `json:"ttl,omitempty"` - PublishZone int `json:"publishZone,omitempty"` -} diff --git a/providers/dns/ddnss/ddnss.go b/providers/dns/ddnss/ddnss.go deleted file mode 100644 index 381151c55..000000000 --- a/providers/dns/ddnss/ddnss.go +++ /dev/null @@ -1,130 +0,0 @@ -// Package ddnss implements a DNS provider for solving the DNS-01 challenge using DynDNS Service. -package ddnss - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/ddnss/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "DDNSS_" - - EnvKey = envNamespace + "KEY" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" - EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - Key string - - PropagationTimeout time.Duration - PollingInterval time.Duration - SequenceInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for DynDNS Service. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvKey) - if err != nil { - return nil, fmt.Errorf("ddnss: %w", err) - } - - config := NewDefaultConfig() - config.Key = values[EnvKey] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for DynDNS Service. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("ddnss: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(&internal.Authentication{Key: config.Key}) - if err != nil { - return nil, fmt.Errorf("ddnss: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - err := d.client.AddTXTRecord(context.Background(), dns01.UnFqdn(info.EffectiveFQDN), info.Value) - if err != nil { - return fmt.Errorf("ddnss: add TXT record: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - err := d.client.RemoveTXTRecord(context.Background(), dns01.UnFqdn(info.EffectiveFQDN)) - if err != nil { - return fmt.Errorf("ddnss: remove TXT record: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -// Sequential All DNS challenges for this provider will be resolved sequentially. -// Returns the interval between each iteration. -func (d *DNSProvider) Sequential() time.Duration { - return d.config.SequenceInterval -} diff --git a/providers/dns/ddnss/ddnss.toml b/providers/dns/ddnss/ddnss.toml deleted file mode 100644 index 0d0a7132c..000000000 --- a/providers/dns/ddnss/ddnss.toml +++ /dev/null @@ -1,23 +0,0 @@ -Name = "DDnss (DynDNS Service)" -Description = '''''' -URL = "https://ddnss.de/" -Code = "ddnss" -Since = "v4.32.0" - -Example = ''' -DDNSS_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns ddnss -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - DDNSS_KEY = "Update key" - [Configuration.Additional] - DDNSS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - DDNSS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - DDNSS_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 60)" - DDNSS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - DDNSS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://ddnss.de/info.php" diff --git a/providers/dns/ddnss/ddnss_test.go b/providers/dns/ddnss/ddnss_test.go deleted file mode 100644 index 5b1d7df58..000000000 --- a/providers/dns/ddnss/ddnss_test.go +++ /dev/null @@ -1,168 +0,0 @@ -package ddnss - -import ( - "net/http/httptest" - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvKey).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvKey: "secret", - }, - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "ddnss: some credentials information are missing: DDNSS_KEY", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - Key string - expected string - }{ - { - desc: "success", - Key: "secret", - }, - { - desc: "missing credentials", - expected: "ddnss: missing credentials", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.Key = test.Key - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.Key = "secret" - config.HTTPClient = server.Client() - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - p.client.BaseURL = server.URL - - return p, nil - }, - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("GET /", - servermock.ResponseFromInternal("success.html"), - servermock.CheckQueryParameter().Strict(). - With("host", "_acme-challenge.example.com"). - With("key", "secret"). - With("txt", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"). - With("txtm", "1"), - ). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("GET /", - servermock.ResponseFromInternal("success.html"), - servermock.CheckQueryParameter().Strict(). - With("host", "_acme-challenge.example.com"). - With("key", "secret"). - With("txtm", "2"), - ). - Build(t) - - err := provider.CleanUp("example.com", "abc", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/ddnss/internal/client.go b/providers/dns/ddnss/internal/client.go deleted file mode 100644 index a0cf4b4a6..000000000 --- a/providers/dns/ddnss/internal/client.go +++ /dev/null @@ -1,137 +0,0 @@ -package internal - -import ( - "context" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "strings" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" - "golang.org/x/net/html" -) - -const defaultBaseURL = "https://ddnss.de/upd.php" - -// Client the DDns API client. -type Client struct { - auth *Authentication - - BaseURL string - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(auth *Authentication) (*Client, error) { - if auth == nil { - return nil, errors.New("credentials missing") - } - - err := auth.validate() - if err != nil { - return nil, err - } - - return &Client{ - auth: auth, - BaseURL: defaultBaseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -func (c *Client) AddTXTRecord(ctx context.Context, host, value string) error { - return c.update(ctx, map[string]string{ - "host": host, - "txt": value, - "txtm": "1", - }) -} - -func (c *Client) RemoveTXTRecord(ctx context.Context, host string) error { - return c.update(ctx, map[string]string{ - "host": host, - "txtm": "2", - }) -} - -func (c *Client) update(ctx context.Context, params map[string]string) error { - endpoint, err := url.Parse(c.BaseURL) - if err != nil { - return err - } - - query := endpoint.Query() - - for k, v := range params { - query.Set(k, v) - } - - c.auth.set(query) - - endpoint.RawQuery = query.Encode() - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) - if err != nil { - return fmt.Errorf("unable to create request: %w", err) - } - - useragent.SetHeader(req.Header) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - raw, _ := io.ReadAll(resp.Body) - - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - content, err := readPage(raw) - if err != nil { - return err - } - - if strings.Contains(content, "Updated 1 hostname.") { - return nil - } - - return fmt.Errorf("unexpected response: %s", content) -} - -func readPage(raw []byte) (string, error) { - page, err := html.Parse(strings.NewReader(string(raw))) - if err != nil { - return "", err - } - - var b strings.Builder - extractText(page, &b) - - return strings.TrimSpace(b.String()), nil -} - -func extractText(n *html.Node, b *strings.Builder) { - if n.Type == html.TextNode { - text := strings.TrimSpace(n.Data) - if text != "" { - b.WriteString(text + " ") - } - } - - for c := n.FirstChild; c != nil; c = c.NextSibling { - extractText(c, b) - } -} diff --git a/providers/dns/ddnss/internal/client_test.go b/providers/dns/ddnss/internal/client_test.go deleted file mode 100644 index 3faddded0..000000000 --- a/providers/dns/ddnss/internal/client_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package internal - -import ( - "net/http/httptest" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient(&Authentication{Key: "secret"}) - if err != nil { - return nil, err - } - - client.BaseURL = server.URL - client.HTTPClient = server.Client() - - return client, nil - }, - ) -} - -func TestClient_AddTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("GET /", - servermock.ResponseFromFixture("success.html"), - servermock.CheckQueryParameter().Strict(). - With("host", "_acme-challenge.example.com"). - With("key", "secret"). - With("txt", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"). - With("txtm", "1"), - ). - Build(t) - - err := client.AddTXTRecord(t.Context(), "_acme-challenge.example.com", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY") - require.NoError(t, err) -} - -func TestClient_RemoveTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("GET /", - servermock.ResponseFromFixture("success.html"), - servermock.CheckQueryParameter().Strict(). - With("host", "_acme-challenge.example.com"). - With("key", "secret"). - With("txtm", "2"), - ). - Build(t) - - err := client.RemoveTXTRecord(t.Context(), "_acme-challenge.example.com") - require.NoError(t, err) -} diff --git a/providers/dns/ddnss/internal/fixtures/error.html b/providers/dns/ddnss/internal/fixtures/error.html deleted file mode 100644 index f0599ad9a..000000000 --- a/providers/dns/ddnss/internal/fixtures/error.html +++ /dev/null @@ -1,12 +0,0 @@ - - - DDNSS - Kostenloser DynDNS Service : Re-ProutDNS v5.01v - - -

-

Error Occurred While Processing Request :

-
- - badysys : Der System Parameter ist ungültig.
- - badauth : Die Authorisation ist fehlgeschlagen. Die Parameter username und/oder password sind falsch.
- - notfqdn : Hostname fehlt oder ist falsch.
- diff --git a/providers/dns/ddnss/internal/fixtures/success.html b/providers/dns/ddnss/internal/fixtures/success.html deleted file mode 100644 index f51957334..000000000 --- a/providers/dns/ddnss/internal/fixtures/success.html +++ /dev/null @@ -1,8 +0,0 @@ - - - DDNSS - Kostenloser DynDNS Service : Re-ProutDNS v5.01v - - -

-

Updated 1 hostname.

- diff --git a/providers/dns/ddnss/internal/types.go b/providers/dns/ddnss/internal/types.go deleted file mode 100644 index 37d41e076..000000000 --- a/providers/dns/ddnss/internal/types.go +++ /dev/null @@ -1,39 +0,0 @@ -package internal - -import ( - "errors" - "net/url" -) - -type Authentication struct { - Username string `url:"user,omitempty"` - Password string `url:"pwd,omitempty"` - Key string `url:"key,omitempty"` -} - -func (a *Authentication) validate() error { - if a.Username == "" && a.Password == "" && a.Key == "" { - return errors.New("missing credentials") - } - - if a.Username != "" && a.Password != "" && a.Key != "" { - return errors.New("only one of username, password or key can be set") - } - - if (a.Username != "" && a.Password == "") || a.Username == "" && a.Password != "" { - return errors.New("username and password must be set together") - } - - return nil -} - -func (a *Authentication) set(query url.Values) { - if a.Key != "" { - query.Set("key", a.Key) - - return - } - - query.Set("user", a.Username) - query.Set("pwd", a.Password) -} diff --git a/providers/dns/derak/derak.go b/providers/dns/derak/derak.go index 78165b936..28262fb04 100644 --- a/providers/dns/derak/derak.go +++ b/providers/dns/derak/derak.go @@ -10,11 +10,9 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/derak/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/miekg/dns" ) @@ -31,8 +29,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string @@ -95,8 +91,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, client: client, @@ -163,7 +157,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() - if !ok { return fmt.Errorf("derak: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) } diff --git a/providers/dns/derak/derak.toml b/providers/dns/derak/derak.toml index 72f49883a..d99e0853d 100644 --- a/providers/dns/derak/derak.toml +++ b/providers/dns/derak/derak.toml @@ -6,7 +6,7 @@ Since = "v4.12.0" Example = ''' DERAK_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns derak -d '*.example.com' -d example.com run +lego --email myemail@example.com --dns derak --domains my.example.org run ''' [Configuration] @@ -14,7 +14,7 @@ lego --dns derak -d '*.example.com' -d example.com run DERAK_API_KEY = "The API key" [Configuration.Additional] DERAK_WEBSITE_ID = "Force the zone/website ID" - DERAK_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 5)" - DERAK_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - DERAK_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - DERAK_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + DERAK_POLLING_INTERVAL = "Time between DNS propagation check" + DERAK_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + DERAK_TTL = "The TTL of the TXT record used for the DNS challenge" + DERAK_HTTP_TIMEOUT = "API request timeout" diff --git a/providers/dns/derak/derak_test.go b/providers/dns/derak/derak_test.go index b83eb2c8c..e58cfb6c1 100644 --- a/providers/dns/derak/derak_test.go +++ b/providers/dns/derak/derak_test.go @@ -33,7 +33,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -93,7 +92,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -107,7 +105,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/derak/internal/client.go b/providers/dns/derak/internal/client.go index 4352e198b..3e7c76fdb 100644 --- a/providers/dns/derak/internal/client.go +++ b/providers/dns/derak/internal/client.go @@ -37,14 +37,13 @@ func NewClient(apiKey string) *Client { // GetRecords gets all records. // Note: the response is not influenced by the query parameters, so the documentation seems wrong. -func (c *Client) GetRecords(ctx context.Context, zoneID string, params *GetRecordsParameters) (*GetRecordsResponse, error) { +func (c Client) GetRecords(ctx context.Context, zoneID string, params *GetRecordsParameters) (*GetRecordsResponse, error) { endpoint := c.baseURL.JoinPath("zones", zoneID, "dnsrecords") v, err := querystring.Values(params) if err != nil { return nil, err } - endpoint.RawQuery = v.Encode() req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) @@ -53,7 +52,6 @@ func (c *Client) GetRecords(ctx context.Context, zoneID string, params *GetRecor } response := &GetRecordsResponse{} - err = c.do(req, response) if err != nil { return nil, err @@ -63,7 +61,7 @@ func (c *Client) GetRecords(ctx context.Context, zoneID string, params *GetRecor } // GetRecord gets a record by ID. -func (c *Client) GetRecord(ctx context.Context, zoneID, recordID string) (*Record, error) { +func (c Client) GetRecord(ctx context.Context, zoneID string, recordID string) (*Record, error) { endpoint := c.baseURL.JoinPath("zones", zoneID, "dnsrecords", recordID) req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) @@ -72,7 +70,6 @@ func (c *Client) GetRecord(ctx context.Context, zoneID, recordID string) (*Recor } response := &Record{} - err = c.do(req, response) if err != nil { return nil, err @@ -82,7 +79,7 @@ func (c *Client) GetRecord(ctx context.Context, zoneID, recordID string) (*Recor } // CreateRecord creates a new record. -func (c *Client) CreateRecord(ctx context.Context, zoneID string, record Record) (*Record, error) { +func (c Client) CreateRecord(ctx context.Context, zoneID string, record Record) (*Record, error) { endpoint := c.baseURL.JoinPath("zones", zoneID, "dnsrecords") req, err := newJSONRequest(ctx, http.MethodPut, endpoint, record) @@ -91,7 +88,6 @@ func (c *Client) CreateRecord(ctx context.Context, zoneID string, record Record) } response := &Record{} - err = c.do(req, response) if err != nil { return nil, err @@ -101,7 +97,7 @@ func (c *Client) CreateRecord(ctx context.Context, zoneID string, record Record) } // EditRecord edits an existing record. -func (c *Client) EditRecord(ctx context.Context, zoneID, recordID string, record Record) (*Record, error) { +func (c Client) EditRecord(ctx context.Context, zoneID string, recordID string, record Record) (*Record, error) { endpoint := c.baseURL.JoinPath("zones", zoneID, "dnsrecords", recordID) req, err := newJSONRequest(ctx, http.MethodPatch, endpoint, record) @@ -110,7 +106,6 @@ func (c *Client) EditRecord(ctx context.Context, zoneID, recordID string, record } response := &Record{} - err = c.do(req, response) if err != nil { return nil, err @@ -120,7 +115,7 @@ func (c *Client) EditRecord(ctx context.Context, zoneID, recordID string, record } // DeleteRecord deletes an existing record. -func (c *Client) DeleteRecord(ctx context.Context, zoneID, recordID string) error { +func (c Client) DeleteRecord(ctx context.Context, zoneID string, recordID string) error { endpoint := c.baseURL.JoinPath("zones", zoneID, "dnsrecords", recordID) req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) @@ -145,14 +140,13 @@ func (c *Client) DeleteRecord(ctx context.Context, zoneID, recordID string) erro // GetZones gets zones. // Note: it's not a part of the official API, there is no documentation about this. // The endpoint comes from UI calls analysis. -func (c *Client) GetZones(ctx context.Context) ([]Zone, error) { +func (c Client) GetZones(ctx context.Context) ([]Zone, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.zoneEndpoint, http.NoBody) if err != nil { return nil, err } response := &APIResponse[[]Zone]{} - err = c.do(req, response) if err != nil { return nil, err @@ -165,7 +159,7 @@ func (c *Client) GetZones(ctx context.Context) ([]Zone, error) { return response.Result, nil } -func (c *Client) do(req *http.Request, result any) error { +func (c Client) do(req *http.Request, result any) error { req.SetBasicAuth("api", c.apiKey) resp, err := c.HTTPClient.Do(req) @@ -227,7 +221,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) var response APIResponse[any] - err := json.Unmarshal(raw, &response) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/derak/internal/client_test.go b/providers/dns/derak/internal/client_test.go index 322a7f48c..3d542e4a7 100644 --- a/providers/dns/derak/internal/client_test.go +++ b/providers/dns/derak/internal/client_test.go @@ -1,39 +1,83 @@ package internal import ( + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" "time" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func setupClient(server *httptest.Server) (*Client, error) { +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + client := NewClient("secret") client.baseURL, _ = url.Parse(server.URL) client.zoneEndpoint = server.URL client.HTTPClient = server.Client() - return client, nil + return client, mux } -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client](setupClient, - servermock.CheckHeader().WithJSONHeaders(). - WithBasicAuth("api", "secret")) +func testHandler(method string, statusCode int, filename string) func(rw http.ResponseWriter, req *http.Request) { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + username, password, ok := req.BasicAuth() + if !ok { + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + if username != "api" { + http.Error(rw, fmt.Sprintf("username: want %s got %s", username, "user"), http.StatusUnauthorized) + return + } + + if password != "secret" { + http.Error(rw, fmt.Sprintf("password: want %s got %s", password, "secret"), http.StatusUnauthorized) + return + } + + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + rw.WriteHeader(statusCode) + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + } } func TestGetRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords", - servermock.ResponseFromFixture("records-GET.json")). - Build(t) + client, mux := setupTest(t) - records, err := client.GetRecords(t.Context(), "47c0ecf6c91243308c649ad1d2d618dd", &GetRecordsParameters{DNSType: "TXT", Content: `"test"'`}) + mux.HandleFunc("/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords", + testHandler(http.MethodGet, http.StatusOK, "records-GET.json")) + + records, err := client.GetRecords(context.Background(), "47c0ecf6c91243308c649ad1d2d618dd", &GetRecordsParameters{DNSType: "TXT", Content: `"test"'`}) require.NoError(t, err) excepted := &GetRecordsResponse{Data: []Record{ @@ -91,23 +135,22 @@ func TestGetRecords(t *testing.T) { } func TestGetRecords_error(t *testing.T) { - client := mockBuilder(). - Route("GET /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client, mux := setupTest(t) - _, err := client.GetRecords(t.Context(), "47c0ecf6c91243308c649ad1d2d618dd", &GetRecordsParameters{DNSType: "TXT", Content: `"test"'`}) + mux.HandleFunc("/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords", + testHandler(http.MethodGet, http.StatusUnauthorized, "error.json")) + + _, err := client.GetRecords(context.Background(), "47c0ecf6c91243308c649ad1d2d618dd", &GetRecordsParameters{DNSType: "TXT", Content: `"test"'`}) require.Error(t, err) } func TestGetRecord(t *testing.T) { - client := mockBuilder(). - Route("GET /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/812bee17a0b440b0bd5ee099a78b839c", - servermock.ResponseFromFixture("record-GET.json")). - Build(t) + client, mux := setupTest(t) - record, err := client.GetRecord(t.Context(), "47c0ecf6c91243308c649ad1d2d618dd", "812bee17a0b440b0bd5ee099a78b839c") + mux.HandleFunc("/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/812bee17a0b440b0bd5ee099a78b839c", + testHandler(http.MethodGet, http.StatusOK, "record-GET.json")) + + record, err := client.GetRecord(context.Background(), "47c0ecf6c91243308c649ad1d2d618dd", "812bee17a0b440b0bd5ee099a78b839c") require.NoError(t, err) excepted := &Record{ @@ -121,22 +164,20 @@ func TestGetRecord(t *testing.T) { } func TestGetRecord_error(t *testing.T) { - client := mockBuilder(). - Route("GET /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client, mux := setupTest(t) - _, err := client.GetRecord(t.Context(), "47c0ecf6c91243308c649ad1d2d618dd", "812bee17a0b440b0bd5ee099a78b839c") + mux.HandleFunc("/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/812bee17a0b440b0bd5ee099a78b839c", + testHandler(http.MethodGet, http.StatusUnauthorized, "error.json")) + + _, err := client.GetRecord(context.Background(), "47c0ecf6c91243308c649ad1d2d618dd", "812bee17a0b440b0bd5ee099a78b839c") require.Error(t, err) } func TestCreateRecord(t *testing.T) { - client := mockBuilder(). - Route("PUT /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords", - servermock.ResponseFromFixture("record-PUT.json"). - WithStatusCode(http.StatusCreated)). - Build(t) + client, mux := setupTest(t) + + mux.HandleFunc("/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords", + testHandler(http.MethodPut, http.StatusCreated, "record-PUT.json")) r := Record{ Type: "TXT", @@ -145,7 +186,7 @@ func TestCreateRecord(t *testing.T) { TTL: 120, } - record, err := client.CreateRecord(t.Context(), "47c0ecf6c91243308c649ad1d2d618dd", r) + record, err := client.CreateRecord(context.Background(), "47c0ecf6c91243308c649ad1d2d618dd", r) require.NoError(t, err) excepted := &Record{ @@ -159,11 +200,10 @@ func TestCreateRecord(t *testing.T) { } func TestCreateRecord_error(t *testing.T) { - client := mockBuilder(). - Route("PUT /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client, mux := setupTest(t) + + mux.HandleFunc("/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords", + testHandler(http.MethodPut, http.StatusUnauthorized, "error.json")) r := Record{ Type: "TXT", @@ -172,17 +212,17 @@ func TestCreateRecord_error(t *testing.T) { TTL: 120, } - _, err := client.CreateRecord(t.Context(), "47c0ecf6c91243308c649ad1d2d618dd", r) + _, err := client.CreateRecord(context.Background(), "47c0ecf6c91243308c649ad1d2d618dd", r) require.Error(t, err) } func TestEditRecord(t *testing.T) { - client := mockBuilder(). - Route("PATCH /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/eebc813de2f94d67b09d91e10e2d65c2", - servermock.ResponseFromFixture("record-PATCH.json")). - Build(t) + client, mux := setupTest(t) - record, err := client.EditRecord(t.Context(), "47c0ecf6c91243308c649ad1d2d618dd", "eebc813de2f94d67b09d91e10e2d65c2", Record{ + mux.HandleFunc("/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/eebc813de2f94d67b09d91e10e2d65c2", + testHandler(http.MethodPatch, http.StatusOK, "record-PATCH.json")) + + record, err := client.EditRecord(context.Background(), "47c0ecf6c91243308c649ad1d2d618dd", "eebc813de2f94d67b09d91e10e2d65c2", Record{ Content: "foo", }) require.NoError(t, err) @@ -198,48 +238,43 @@ func TestEditRecord(t *testing.T) { } func TestEditRecord_error(t *testing.T) { - client := mockBuilder(). - Route("PATCH /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/eebc813de2f94d67b09d91e10e2d65c2", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client, mux := setupTest(t) - _, err := client.EditRecord(t.Context(), "47c0ecf6c91243308c649ad1d2d618dd", "eebc813de2f94d67b09d91e10e2d65c2", Record{ + mux.HandleFunc("/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/eebc813de2f94d67b09d91e10e2d65c2", + testHandler(http.MethodPatch, http.StatusUnauthorized, "error.json")) + + _, err := client.EditRecord(context.Background(), "47c0ecf6c91243308c649ad1d2d618dd", "eebc813de2f94d67b09d91e10e2d65c2", Record{ Content: "foo", }) require.Error(t, err) } func TestDeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/653464211b7447a1bee6b8fcb9fb86df", - servermock.ResponseFromFixture("record-DELETE.json")). - Build(t) + client, mux := setupTest(t) - err := client.DeleteRecord(t.Context(), "47c0ecf6c91243308c649ad1d2d618dd", "653464211b7447a1bee6b8fcb9fb86df") + mux.HandleFunc("/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/653464211b7447a1bee6b8fcb9fb86df", + testHandler(http.MethodDelete, http.StatusOK, "record-DELETE.json")) + + err := client.DeleteRecord(context.Background(), "47c0ecf6c91243308c649ad1d2d618dd", "653464211b7447a1bee6b8fcb9fb86df") require.NoError(t, err) } func TestDeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/653464211b7447a1bee6b8fcb9fb86df", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client, mux := setupTest(t) - err := client.DeleteRecord(t.Context(), "47c0ecf6c91243308c649ad1d2d618dd", "653464211b7447a1bee6b8fcb9fb86df") + mux.HandleFunc("/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/653464211b7447a1bee6b8fcb9fb86df", + testHandler(http.MethodDelete, http.StatusUnauthorized, "error.json")) + + err := client.DeleteRecord(context.Background(), "47c0ecf6c91243308c649ad1d2d618dd", "653464211b7447a1bee6b8fcb9fb86df") require.Error(t, err) } func TestGetZones(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient, - servermock.CheckHeader(). - WithBasicAuth("api", "secret"), - ). - Route("GET /", servermock.ResponseFromFixture("service-cdn-zones.json")). - Build(t) + client, mux := setupTest(t) - zones, err := client.GetZones(t.Context()) + mux.HandleFunc("/", testHandler(http.MethodGet, http.StatusOK, "service-cdn-zones.json")) + + zones, err := client.GetZones(context.Background()) require.NoError(t, err) excepted := []Zone{{ @@ -268,11 +303,10 @@ func TestGetZones(t *testing.T) { } func TestGetZones_error(t *testing.T) { - client := mockBuilder(). - Route("GET /", servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client, mux := setupTest(t) - _, err := client.GetZones(t.Context()) + mux.HandleFunc("/", testHandler(http.MethodGet, http.StatusUnauthorized, "error.json")) + + _, err := client.GetZones(context.Background()) require.Error(t, err) } diff --git a/providers/dns/derak/internal/types.go b/providers/dns/derak/internal/types.go index 02116314f..15ed00617 100644 --- a/providers/dns/derak/internal/types.go +++ b/providers/dns/derak/internal/types.go @@ -46,7 +46,7 @@ type Zone struct { HumanReadable string `json:"humanReadable,omitempty"` Serial string `json:"serial,omitempty"` CreationTime int64 `json:"creationTime,omitempty"` - CreationTimeDate time.Time `json:"creationTimeDate,omitzero"` + CreationTimeDate time.Time `json:"creationTimeDate,omitempty"` Status string `json:"status,omitempty"` IsMoved bool `json:"is_moved,omitempty"` Paused bool `json:"paused,omitempty"` diff --git a/providers/dns/desec/desec.go b/providers/dns/desec/desec.go index 9cc54f65e..c86e0ceb8 100644 --- a/providers/dns/desec/desec.go +++ b/providers/dns/desec/desec.go @@ -9,10 +9,8 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/nrdcg/desec" ) @@ -32,8 +30,6 @@ const ( // https://desec.readthedocs.io/_/downloads/en/latest/pdf/ const defaultTTL int = 3600 -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Token string @@ -89,9 +85,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config.HTTPClient != nil { opts.HTTPClient = config.HTTPClient } - - opts.HTTPClient = clientdebug.Wrap(opts.HTTPClient) - opts.Logger = log.Default() client := desec.New(config.Token, opts) @@ -109,6 +102,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { func (d *DNSProvider) Present(domain, token, keyAuth string) error { ctx := context.Background() info := dns01.GetChallengeInfo(domain, keyAuth) + quotedValue := fmt.Sprintf(`%q`, info.Value) authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { @@ -122,8 +116,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { domainName := dns01.UnFqdn(authZone) - quotedValue := fmt.Sprintf(`%q`, info.Value) - rrSet, err := d.client.Records.Get(ctx, domainName, recordName, "TXT") if err != nil { var nf *desec.NotFoundError @@ -180,7 +172,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } records := make([]string, 0) - for _, record := range rrSet.Records { if record != fmt.Sprintf(`%q`, info.Value) { records = append(records, record) diff --git a/providers/dns/desec/desec.toml b/providers/dns/desec/desec.toml index f7e66ae07..4bfbf0fb9 100644 --- a/providers/dns/desec/desec.toml +++ b/providers/dns/desec/desec.toml @@ -6,17 +6,17 @@ Since = "v3.7.0" Example = ''' DESEC_TOKEN=x-xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --dns desec -d '*.example.com' -d example.com run +lego --email you@example.com --dns desec --domains my.example.org run ''' [Configuration] [Configuration.Credentials] DESEC_TOKEN = "Domain token" [Configuration.Additional] - DESEC_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 4)" - DESEC_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - DESEC_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)" - DESEC_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + DESEC_POLLING_INTERVAL = "Time between DNS propagation check" + DESEC_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + DESEC_TTL = "The TTL of the TXT record used for the DNS challenge" + DESEC_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://desec.readthedocs.io/en/latest/" diff --git a/providers/dns/desec/desec_test.go b/providers/dns/desec/desec_test.go index 93d9bd010..f91f9e82a 100644 --- a/providers/dns/desec/desec_test.go +++ b/providers/dns/desec/desec_test.go @@ -36,7 +36,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -94,7 +93,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -108,7 +106,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/designate/designate.go b/providers/dns/designate/designate.go index 41bf251f6..949263316 100644 --- a/providers/dns/designate/designate.go +++ b/providers/dns/designate/designate.go @@ -10,7 +10,6 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/gophercloud/gophercloud" @@ -28,8 +27,6 @@ const ( EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvZoneName = envNamespace + "ZONE_NAME" - envNamespaceClient = "OS_" EnvAuthURL = envNamespaceClient + "AUTH_URL" @@ -45,11 +42,8 @@ const ( EnvCloud = envNamespaceClient + "CLOUD" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { - ZoneName string PropagationTimeout time.Duration PollingInterval time.Duration TTL int @@ -59,7 +53,6 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - ZoneName: env.GetOrFile(EnvZoneName), TTL: env.GetOrDefaultInt(EnvTTL, 10), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), @@ -68,9 +61,8 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - config *Config - client *gophercloud.ServiceClient - + config *Config + client *gophercloud.ServiceClient dnsEntriesMu sync.Mutex } @@ -86,6 +78,7 @@ func NewDNSProvider() (*DNSProvider, error) { opts, erro := clientconfig.AuthOptions(&clientconfig.ClientOpts{ Cloud: val[EnvCloud], }) + if erro != nil { return nil, fmt.Errorf("designate: %w", erro) } @@ -134,12 +127,12 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) - zone, err := d.getZoneName(info.EffectiveFQDN) + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { - return fmt.Errorf("designate: %w", err) + return fmt.Errorf("designate: could not find zone for domain %q: %w", domain, err) } - zoneID, err := d.getZoneID(zone) + zoneID, err := d.getZoneID(authZone) if err != nil { return fmt.Errorf("designate: couldn't get zone ID in Present: %w", err) } @@ -174,12 +167,12 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) - zone, err := d.getZoneName(info.EffectiveFQDN) + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { - return fmt.Errorf("designate: %w", err) + return fmt.Errorf("designate: could not find zone for domain %q: %w", domain, err) } - zoneID, err := d.getZoneID(zone) + zoneID, err := d.getZoneID(authZone) if err != nil { return fmt.Errorf("designate: couldn't get zone ID in CleanUp: %w", err) } @@ -202,7 +195,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("designate: error for %s in CleanUp: %w", info.EffectiveFQDN, err) } - return nil } @@ -242,20 +234,14 @@ func (d *DNSProvider) updateRecord(record *recordsets.RecordSet, value string) e } result := recordsets.Update(d.client, record.ZoneID, record.ID, updateOpts) - return result.Err } func (d *DNSProvider) getZoneID(wanted string) (string, error) { - listOpts := zones.ListOpts{ - Name: wanted, - } - - allPages, err := zones.List(d.client, listOpts).AllPages() + allPages, err := zones.List(d.client, nil).AllPages() if err != nil { return "", err } - allZones, err := zones.ExtractZones(allPages) if err != nil { return "", err @@ -266,21 +252,14 @@ func (d *DNSProvider) getZoneID(wanted string) (string, error) { return zone.ID, nil } } - return "", fmt.Errorf("zone id not found for %s", wanted) } func (d *DNSProvider) getRecord(zoneID, wanted string) (*recordsets.RecordSet, error) { - listOpts := recordsets.ListOpts{ - Name: wanted, - Type: "TXT", - } - - allPages, err := recordsets.ListByZone(d.client, zoneID, listOpts).AllPages() + allPages, err := recordsets.ListByZone(d.client, zoneID, nil).AllPages() if err != nil { return nil, err } - allRecords, err := recordsets.ExtractRecordSets(allPages) if err != nil { return nil, err @@ -294,20 +273,3 @@ func (d *DNSProvider) getRecord(zoneID, wanted string) (*recordsets.RecordSet, e return nil, nil } - -func (d *DNSProvider) getZoneName(fqdn string) (string, error) { - if d.config.ZoneName != "" { - return d.config.ZoneName, nil - } - - authZone, err := dns01.FindZoneByFqdn(fqdn) - if err != nil { - return "", fmt.Errorf("could not find zone for %s: %w", fqdn, err) - } - - if authZone == "" { - return "", errors.New("empty zone name") - } - - return authZone, nil -} diff --git a/providers/dns/designate/designate.toml b/providers/dns/designate/designate.toml index a36034f64..55a1cd3c7 100644 --- a/providers/dns/designate/designate.toml +++ b/providers/dns/designate/designate.toml @@ -7,7 +7,7 @@ Since = "v2.2.0" Example = ''' # With a `clouds.yaml` OS_CLOUD=my_openstack \ -lego --dns designate -d '*.example.com' -d example.com run +lego --email you@example.com --dns designate --domains my.example.org run # or @@ -16,7 +16,7 @@ OS_REGION_NAME=RegionOne \ OS_PROJECT_ID=23d4522a987d4ab529f722a007c27846 OS_USERNAME=myuser \ OS_PASSWORD=passw0rd \ -lego --dns designate -d '*.example.com' -d example.com run +lego --email you@example.com --dns designate --domains my.example.org run # or @@ -25,7 +25,7 @@ OS_REGION_NAME=RegionOne \ OS_AUTH_TYPE=v3applicationcredential \ OS_APPLICATION_CREDENTIAL_ID=imn74uq0or7dyzz20dwo1ytls4me8dry \ OS_APPLICATION_CREDENTIAL_SECRET=68FuSPSdQqkFQYH5X1OoriEIJOwyLtQ8QSqXZOc9XxFK1A9tzZT6He2PfPw0OMja \ -lego --dns designate -d '*.example.com' -d example.com run +lego --email you@example.com --dns designate --domains my.example.org run ''' Additional = ''' @@ -63,10 +63,9 @@ Public cloud providers with support for Designate: [Configuration.Additional] OS_PROJECT_ID = "Project ID" OS_TENANT_NAME = "Tenant name (deprecated see OS_PROJECT_NAME and OS_PROJECT_ID)" - DESIGNATE_ZONE_NAME = "The zone name to use in the OpenStack Project to manage TXT records." - DESIGNATE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" - DESIGNATE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 600)" - DESIGNATE_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 10)" + DESIGNATE_POLLING_INTERVAL = "Time between DNS propagation check" + DESIGNATE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + DESIGNATE_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://docs.openstack.org/designate/latest/" diff --git a/providers/dns/designate/designate_test.go b/providers/dns/designate/designate_test.go index e5edf81f8..881faeef1 100644 --- a/providers/dns/designate/designate_test.go +++ b/providers/dns/designate/designate_test.go @@ -105,7 +105,6 @@ func TestNewDNSProvider_fromEnv(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -193,7 +192,6 @@ func TestNewDNSProvider_fromCloud(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(map[string]string{ @@ -267,10 +265,10 @@ func TestNewDNSProviderConfig(t *testing.T) { func createCloudsYaml(t *testing.T, cloudName string, cloud clientconfig.Cloud) string { t.Helper() - file, err := os.CreateTemp(t.TempDir(), "lego_test") + file, err := os.CreateTemp("", "lego_test") require.NoError(t, err) - t.Cleanup(func() { _ = file.Close() }) + t.Cleanup(func() { _ = os.RemoveAll(file.Name()) }) clouds := clientconfig.Clouds{ Clouds: map[string]clientconfig.Cloud{ @@ -333,7 +331,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -347,7 +344,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/digitalocean/digitalocean.go b/providers/dns/digitalocean/digitalocean.go index 26c6fb9d4..792c11f35 100644 --- a/providers/dns/digitalocean/digitalocean.go +++ b/providers/dns/digitalocean/digitalocean.go @@ -10,11 +10,9 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/digitalocean/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -30,8 +28,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string @@ -47,7 +43,7 @@ func NewDefaultConfig() *Config { return &Config{ BaseURL: env.GetOrDefaultString(EnvAPIUrl, internal.DefaultBaseURL), TTL: env.GetOrDefaultInt(EnvTTL, 30), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 60*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), @@ -89,15 +85,10 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("digitalocean: credentials missing") } - client := internal.NewClient( - clientdebug.Wrap( - internal.OAuthStaticAccessToken(config.HTTPClient, config.AuthToken), - ), - ) + client := internal.NewClient(internal.OAuthStaticAccessToken(config.HTTPClient, config.AuthToken)) if config.BaseURL != "" { var err error - client.BaseURL, err = url.Parse(config.BaseURL) if err != nil { return nil, fmt.Errorf("digitalocean: %w", err) @@ -121,7 +112,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(info.EffectiveFQDN)) if err != nil { return fmt.Errorf("digitalocean: could not find zone for domain %q: %w", domain, err) } @@ -153,7 +144,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() - if !ok { return fmt.Errorf("digitalocean: unknown record ID for '%s'", info.EffectiveFQDN) } diff --git a/providers/dns/digitalocean/digitalocean.toml b/providers/dns/digitalocean/digitalocean.toml index 8f9107c26..11b7fa5d8 100644 --- a/providers/dns/digitalocean/digitalocean.toml +++ b/providers/dns/digitalocean/digitalocean.toml @@ -6,7 +6,7 @@ Since = "v0.3.0" Example = ''' DO_AUTH_TOKEN=xxxxxx \ -lego --dns digitalocean -d '*.example.com' -d example.com run +lego --email you@example.com --dns digitalocean --domains my.example.org run ''' [Configuration] @@ -14,10 +14,10 @@ lego --dns digitalocean -d '*.example.com' -d example.com run DO_AUTH_TOKEN = "Authentication token" [Configuration.Additional] DO_API_URL = "The URL of the API" - DO_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 5)" - DO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - DO_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 30)" - DO_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + DO_POLLING_INTERVAL = "Time between DNS propagation check" + DO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + DO_TTL = "The TTL of the TXT record used for the DNS challenge" + DO_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://developers.digitalocean.com/documentation/v2/#domain-records" diff --git a/providers/dns/digitalocean/digitalocean_test.go b/providers/dns/digitalocean/digitalocean_test.go index d066e12db..bfd2d68c0 100644 --- a/providers/dns/digitalocean/digitalocean_test.go +++ b/providers/dns/digitalocean/digitalocean_test.go @@ -1,30 +1,36 @@ package digitalocean import ( + "bytes" + "fmt" + "io" "net/http" "net/http/httptest" "testing" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var envTest = tester.NewEnvTest(EnvAuthToken) -func mockProvider() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.AuthToken = "asdf1234" - config.BaseURL = server.URL - config.HTTPClient = server.Client() +func setupTest(t *testing.T) (*DNSProvider, *http.ServeMux) { + t.Helper() - return NewDNSProviderConfig(config) - }, - servermock.CheckHeader(). - WithJSONHeaders(). - With("Authorization", "Bearer asdf1234")) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + config := NewDefaultConfig() + config.AuthToken = "asdf1234" + config.BaseURL = server.URL + config.HTTPClient = server.Client() + + provider, err := NewDNSProviderConfig(config) + require.NoError(t, err) + + return provider, mux } func TestNewDNSProvider(t *testing.T) { @@ -51,7 +57,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -106,9 +111,26 @@ func TestNewDNSProviderConfig(t *testing.T) { } func TestDNSProvider_Present(t *testing.T) { - provider := mockProvider(). - Route("POST /v2/domains/example.com/records", - servermock.RawStringResponse(`{ + provider, mux := setupTest(t) + + mux.HandleFunc("/v2/domains/example.com/records", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method, "method") + + assert.Equal(t, "application/json", r.Header.Get("Accept"), "Accept") + assert.Equal(t, "application/json", r.Header.Get("Content-Type"), "Content-Type") + assert.Equal(t, "Bearer asdf1234", r.Header.Get("Authorization"), "Authorization") + + reqBody, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + expectedReqBody := `{"type":"TXT","name":"_acme-challenge.example.com.","data":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","ttl":30}` + assert.Equal(t, expectedReqBody, string(bytes.TrimSpace(reqBody))) + + w.WriteHeader(http.StatusCreated) + _, err = fmt.Fprintf(w, `{ "domain_record": { "id": 1234567, "type": "TXT", @@ -118,26 +140,36 @@ func TestDNSProvider_Present(t *testing.T) { "port": null, "weight": null } - }`). - WithStatusCode(http.StatusCreated), - servermock.CheckRequestJSONBody(`{"type":"TXT","name":"_acme-challenge.example.com.","data":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","ttl":30}`)). - Build(t) + }`) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) err := provider.Present("example.com", "", "foobar") require.NoError(t, err) } func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockProvider(). - Route("DELETE /v2/domains/example.com/records/1234567", - servermock.Noop(). - WithStatusCode(http.StatusNoContent)). - Build(t) + provider, mux := setupTest(t) + + mux.HandleFunc("/v2/domains/example.com/records/1234567", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodDelete, r.Method, "method") + + assert.Equal(t, "/v2/domains/example.com/records/1234567", r.URL.Path, "Path") + + assert.Equal(t, "application/json", r.Header.Get("Accept"), "Accept") + assert.Equal(t, "application/json", r.Header.Get("Content-Type"), "Content-Type") + assert.Equal(t, "Bearer asdf1234", r.Header.Get("Authorization"), "Authorization") + + w.WriteHeader(http.StatusNoContent) + }) provider.recordIDsMu.Lock() provider.recordIDs["token"] = 1234567 provider.recordIDsMu.Unlock() err := provider.CleanUp("example.com", "token", "") - require.NoError(t, err) + require.NoError(t, err, "fail to remove TXT record") } diff --git a/providers/dns/digitalocean/internal/client.go b/providers/dns/digitalocean/internal/client.go index 395de478c..e7dd181b2 100644 --- a/providers/dns/digitalocean/internal/client.go +++ b/providers/dns/digitalocean/internal/client.go @@ -45,7 +45,6 @@ func (c *Client) AddTxtRecord(ctx context.Context, zone string, record Record) ( } respData := &TxtRecordResponse{} - err = c.do(req, respData) if err != nil { return nil, err @@ -121,7 +120,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) var errInfo APIError - err := json.Unmarshal(raw, &errInfo) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/digitalocean/internal/client_test.go b/providers/dns/digitalocean/internal/client_test.go index 65ce5dfaa..081e1a109 100644 --- a/providers/dns/digitalocean/internal/client_test.go +++ b/providers/dns/digitalocean/internal/client_test.go @@ -1,35 +1,95 @@ package internal import ( + "bytes" + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient(OAuthStaticAccessToken(server.Client(), "secret")) - client.BaseURL, _ = url.Parse(server.URL) +func setupTest(t *testing.T, pattern string, handler http.HandlerFunc) *Client { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("Bearer secret")) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient(OAuthStaticAccessToken(server.Client(), "secret")) + client.BaseURL, _ = url.Parse(server.URL) + + mux.HandleFunc(pattern, handler) + + return client +} + +func checkHeader(req *http.Request, name, value string) error { + val := req.Header.Get(name) + if val != value { + return fmt.Errorf("invalid header value, got: %s want %s", val, value) + } + return nil +} + +func writeFixture(rw http.ResponseWriter, filename string) { + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, _ = io.Copy(rw, file) } func TestClient_AddTxtRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /v2/domains/example.com/records", - servermock.ResponseFromFixture("domains-records_POST.json"). - WithStatusCode(http.StatusCreated), - servermock.CheckRequestJSONBody(`{"type":"TXT","name":"_acme-challenge.example.com.","data":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","ttl":30}`)). - Build(t) + client := setupTest(t, "/v2/domains/example.com/records", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + err := checkHeader(req, "Accept", "application/json") + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + err = checkHeader(req, "Content-Type", "application/json") + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + err = checkHeader(req, "Authorization", "Bearer secret") + if err != nil { + http.Error(rw, err.Error(), http.StatusUnauthorized) + return + } + + reqBody, err := io.ReadAll(req.Body) + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + expectedReqBody := `{"type":"TXT","name":"_acme-challenge.example.com.","data":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","ttl":30}` + if expectedReqBody != string(bytes.TrimSpace(reqBody)) { + http.Error(rw, fmt.Sprintf("unexpected request body: %s", string(bytes.TrimSpace(reqBody))), http.StatusBadRequest) + return + } + + rw.WriteHeader(http.StatusCreated) + writeFixture(rw, "domains-records_POST.json") + }) record := Record{ Type: "TXT", @@ -38,7 +98,7 @@ func TestClient_AddTxtRecord(t *testing.T) { TTL: 30, } - newRecord, err := client.AddTxtRecord(t.Context(), "example.com", record) + newRecord, err := client.AddTxtRecord(context.Background(), "example.com", record) require.NoError(t, err) expected := &TxtRecordResponse{DomainRecord: Record{ @@ -53,12 +113,27 @@ func TestClient_AddTxtRecord(t *testing.T) { } func TestClient_RemoveTxtRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /v2/domains/example.com/records/1234567", - servermock.ResponseFromFixture("domains-records_POST.json"). - WithStatusCode(http.StatusNoContent)). - Build(t) + client := setupTest(t, "/v2/domains/example.com/records/1234567", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodDelete { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } - err := client.RemoveTxtRecord(t.Context(), "example.com", 1234567) + err := checkHeader(req, "Accept", "application/json") + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + err = checkHeader(req, "Authorization", "Bearer secret") + if err != nil { + http.Error(rw, err.Error(), http.StatusUnauthorized) + return + } + + rw.WriteHeader(http.StatusNoContent) + }) + + err := client.RemoveTxtRecord(context.Background(), "example.com", 1234567) require.NoError(t, err) } diff --git a/providers/dns/directadmin/directadmin.go b/providers/dns/directadmin/directadmin.go deleted file mode 100644 index 8dfa132ae..000000000 --- a/providers/dns/directadmin/directadmin.go +++ /dev/null @@ -1,186 +0,0 @@ -package directadmin - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/directadmin/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "DIRECTADMIN_" - - EnvAPIURL = envNamespace + "API_URL" - EnvUsername = envNamespace + "USERNAME" - EnvPassword = envNamespace + "PASSWORD" - EnvZoneName = envNamespace + "ZONE_NAME" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - BaseURL string - Username string - Password string - - ZoneName string - - TTL int - PropagationTimeout time.Duration - PollingInterval time.Duration - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - ZoneName: env.GetOrFile(EnvZoneName), - TTL: env.GetOrDefaultInt(EnvTTL, 30), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 60*time.Second), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - client *internal.Client - config *Config -} - -// NewDNSProvider returns a DNSProvider instance configured for DirectAdmin. -// Credentials must be passed in the environment variables: -// DIRECTADMIN_API_URL, DIRECTADMIN_USERNAME, DIRECTADMIN_PASSWORD. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIURL, EnvUsername, EnvPassword) - if err != nil { - return nil, fmt.Errorf("directadmin: %w", err) - } - - config := NewDefaultConfig() - config.BaseURL = values[EnvAPIURL] - config.Username = values[EnvUsername] - config.Password = values[EnvPassword] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for DirectAdmin. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config.BaseURL == "" { - return nil, errors.New("directadmin: missing API URL") - } - - if config.Username == "" || config.Password == "" { - return nil, errors.New("directadmin: some credentials information are missing") - } - - client, err := internal.NewClient(config.BaseURL, config.Username, config.Password) - if err != nil { - return nil, fmt.Errorf("directadmin: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{client: client, config: config}, nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := d.getZoneName(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("directadmin: [domain: %q] %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("directadmin: %w", err) - } - - record := internal.Record{ - Name: subDomain, - Type: "TXT", - Value: info.Value, - TTL: d.config.TTL, - } - - err = d.client.SetRecord(context.Background(), dns01.UnFqdn(authZone), record) - if err != nil { - return fmt.Errorf("directadmin: set record for zone %s and subdomain %s: %w", authZone, subDomain, err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := d.getZoneName(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("directadmin: [domain: %q] %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("directadmin: %w", err) - } - - record := internal.Record{ - Name: subDomain, - Type: "TXT", - Value: info.Value, - } - - err = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), record) - if err != nil { - return fmt.Errorf("directadmin: delete record for zone %s and subdomain %s: %w", authZone, subDomain, err) - } - - return nil -} - -func (d *DNSProvider) getZoneName(fqdn string) (string, error) { - if d.config.ZoneName != "" { - return d.config.ZoneName, nil - } - - authZone, err := dns01.FindZoneByFqdn(fqdn) - if err != nil { - return "", fmt.Errorf("could not find zone for %s: %w", fqdn, err) - } - - if authZone == "" { - return "", errors.New("empty zone name") - } - - return authZone, nil -} diff --git a/providers/dns/directadmin/directadmin.toml b/providers/dns/directadmin/directadmin.toml deleted file mode 100644 index 294eaca1c..000000000 --- a/providers/dns/directadmin/directadmin.toml +++ /dev/null @@ -1,27 +0,0 @@ -Name = "DirectAdmin" -Description = '''''' -URL = "https://www.directadmin.com" -Code = "directadmin" -Since = "v4.18.0" - -Example = ''' -DIRECTADMIN_API_URL="http://example.com:2222" \ -DIRECTADMIN_USERNAME=xxxx \ -DIRECTADMIN_PASSWORD=yyy \ -lego --dns directadmin -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - DIRECTADMIN_API_URL = "URL of the API" - DIRECTADMIN_USERNAME = "API username" - DIRECTADMIN_PASSWORD = "API password" - [Configuration.Additional] - DIRECTADMIN_ZONE_NAME = "Zone name used to add the TXT record" - DIRECTADMIN_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 5)" - DIRECTADMIN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - DIRECTADMIN_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 30)" - DIRECTADMIN_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://www.directadmin.com/api.php" diff --git a/providers/dns/directadmin/directadmin_test.go b/providers/dns/directadmin/directadmin_test.go deleted file mode 100644 index aed3ba505..000000000 --- a/providers/dns/directadmin/directadmin_test.go +++ /dev/null @@ -1,159 +0,0 @@ -package directadmin - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAPIURL, EnvUsername, EnvPassword).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAPIURL: "https://example.com:2222", - EnvUsername: "test", - EnvPassword: "secret", - }, - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "directadmin: some credentials information are missing: DIRECTADMIN_API_URL,DIRECTADMIN_USERNAME,DIRECTADMIN_PASSWORD", - }, - { - desc: "missing API URL", - envVars: map[string]string{ - EnvUsername: "test", - EnvPassword: "secret", - }, - expected: "directadmin: some credentials information are missing: DIRECTADMIN_API_URL", - }, - { - desc: "missing username", - envVars: map[string]string{ - EnvAPIURL: "https://example.com:2222", - EnvPassword: "secret", - }, - expected: "directadmin: some credentials information are missing: DIRECTADMIN_USERNAME", - }, - { - desc: "missing password", - envVars: map[string]string{ - EnvAPIURL: "https://example.com:2222", - EnvUsername: "test", - }, - expected: "directadmin: some credentials information are missing: DIRECTADMIN_PASSWORD", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.client) - require.NotNil(t, p.config) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - baseURL string - username string - password string - expected string - }{ - { - desc: "success", - baseURL: "https://example.com", - username: "test", - password: "secret", - }, - { - desc: "missing API URL", - expected: "directadmin: missing API URL", - }, - { - desc: "missing username", - baseURL: "https://example.com", - expected: "directadmin: some credentials information are missing", - }, - { - desc: "missing password", - baseURL: "https://example.com", - username: "test", - expected: "directadmin: some credentials information are missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.BaseURL = test.baseURL - config.Username = test.username - config.Password = test.password - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.client) - require.NotNil(t, p.config) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/directadmin/internal/client.go b/providers/dns/directadmin/internal/client.go deleted file mode 100644 index 64409a79d..000000000 --- a/providers/dns/directadmin/internal/client.go +++ /dev/null @@ -1,104 +0,0 @@ -package internal - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "strings" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - querystring "github.com/google/go-querystring/query" -) - -// Client the Direct Admin API client. -type Client struct { - baseURL *url.URL - HTTPClient *http.Client - - username string - password string -} - -// NewClient creates a new Client. -func NewClient(baseURL, username, password string) (*Client, error) { - api, err := url.Parse(baseURL) - if err != nil { - return nil, err - } - - return &Client{ - baseURL: api, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - username: username, - password: password, - }, nil -} - -func (c *Client) SetRecord(ctx context.Context, domain string, record Record) error { - data, err := querystring.Values(record) - if err != nil { - return err - } - - data.Set("action", "add") - - return c.do(ctx, domain, data) -} - -func (c *Client) DeleteRecord(ctx context.Context, domain string, record Record) error { - data, err := querystring.Values(record) - if err != nil { - return err - } - - data.Set("action", "delete") - - return c.do(ctx, domain, data) -} - -func (c *Client) do(ctx context.Context, domain string, data url.Values) error { - endpoint := c.baseURL.JoinPath("CMD_API_DNS_CONTROL") - - query := endpoint.Query() - query.Set("domain", domain) - query.Set("json", "yes") - endpoint.RawQuery = query.Encode() - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), strings.NewReader(data.Encode())) - if err != nil { - return fmt.Errorf("unable to create request: %w", err) - } - - req.SetBasicAuth(c.username, c.password) - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - return parseError(req, resp) - } - - return nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errInfo APIError - - err := json.Unmarshal(raw, &errInfo) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return fmt.Errorf("[status code %d] %w", resp.StatusCode, errInfo) -} diff --git a/providers/dns/directadmin/internal/client_test.go b/providers/dns/directadmin/internal/client_test.go deleted file mode 100644 index 759a7fb4e..000000000 --- a/providers/dns/directadmin/internal/client_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package internal - -import ( - "fmt" - "net/http" - "net/http/httptest" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, _ := NewClient(server.URL, "user", "secret") - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader(). - WithContentTypeFromURLEncoded()) -} - -func newAPIError(reason string, a ...any) APIError { - return APIError{ - Message: "Cannot View Dns Record", - Result: fmt.Sprintf(reason, a...), - } -} - -func TestClient_SetRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /CMD_API_DNS_CONTROL", nil, - servermock.CheckQueryParameter().Strict(). - With("domain", "example.com"). - With("json", "yes"), - servermock.CheckForm().UsePostForm().Strict(). - With("action", "add"). - With("name", "foo"). - With("type", "TXT"). - With("value", "txtTXTtxt"). - With("ttl", "123"), - ). - Build(t) - - record := Record{ - Name: "foo", - Type: "TXT", - Value: "txtTXTtxt", - TTL: 123, - } - - err := client.SetRecord(t.Context(), "example.com", record) - require.NoError(t, err) -} - -func TestClient_SetRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /CMD_API_DNS_CONTROL", - servermock.JSONEncode(newAPIError("OOPS")). - WithStatusCode(http.StatusInternalServerError)). - Build(t) - - record := Record{ - Name: "foo", - Type: "TXT", - Value: "txtTXTtxt", - TTL: 123, - } - - err := client.SetRecord(t.Context(), "example.com", record) - require.EqualError(t, err, "[status code 500] Cannot View Dns Record: OOPS") -} - -func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /CMD_API_DNS_CONTROL", nil, - servermock.CheckQueryParameter().Strict(). - With("domain", "example.com"). - With("json", "yes"), - servermock.CheckForm().UsePostForm().Strict(). - With("action", "delete"). - With("name", "foo"). - With("type", "TXT"). - With("value", "txtTXTtxt"), - ). - Build(t) - - record := Record{ - Name: "foo", - Type: "TXT", - Value: "txtTXTtxt", - } - - err := client.DeleteRecord(t.Context(), "example.com", record) - require.NoError(t, err) -} - -func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /CMD_API_DNS_CONTROL", - servermock.JSONEncode(newAPIError("OOPS")). - WithStatusCode(http.StatusInternalServerError)). - Build(t) - - record := Record{ - Name: "foo", - Type: "TXT", - Value: "txtTXTtxt", - } - - err := client.DeleteRecord(t.Context(), "example.com", record) - require.EqualError(t, err, "[status code 500] Cannot View Dns Record: OOPS") -} diff --git a/providers/dns/directadmin/internal/types.go b/providers/dns/directadmin/internal/types.go deleted file mode 100644 index 461e36f98..000000000 --- a/providers/dns/directadmin/internal/types.go +++ /dev/null @@ -1,21 +0,0 @@ -package internal - -import "fmt" - -// Record represents a DNS record. -type Record struct { - Name string `url:"name,omitempty"` - Type string `url:"type,omitempty"` - Value string `url:"value,omitempty"` - TTL int `url:"ttl,omitempty"` -} - -// APIError represents a API error. -type APIError struct { - Message string `json:"error,omitempty"` - Result string `json:"result,omitempty"` -} - -func (a APIError) Error() string { - return fmt.Sprintf("%s: %s", a.Message, a.Result) -} diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/dns_providers.go similarity index 67% rename from providers/dns/zz_gen_dns_providers.go rename to providers/dns/dns_providers.go index 9c4bc9e61..ff26a4921 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/dns_providers.go @@ -1,33 +1,20 @@ -// Code generated by 'make generate-dns'; DO NOT EDIT. - package dns import ( "fmt" "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/providers/dns/acmedns" - "github.com/go-acme/lego/v4/providers/dns/active24" "github.com/go-acme/lego/v4/providers/dns/alidns" - "github.com/go-acme/lego/v4/providers/dns/aliesa" "github.com/go-acme/lego/v4/providers/dns/allinkl" - "github.com/go-acme/lego/v4/providers/dns/alwaysdata" - "github.com/go-acme/lego/v4/providers/dns/anexia" - "github.com/go-acme/lego/v4/providers/dns/artfiles" "github.com/go-acme/lego/v4/providers/dns/arvancloud" "github.com/go-acme/lego/v4/providers/dns/auroradns" "github.com/go-acme/lego/v4/providers/dns/autodns" - "github.com/go-acme/lego/v4/providers/dns/axelname" - "github.com/go-acme/lego/v4/providers/dns/azion" "github.com/go-acme/lego/v4/providers/dns/azure" "github.com/go-acme/lego/v4/providers/dns/azuredns" - "github.com/go-acme/lego/v4/providers/dns/baiducloud" - "github.com/go-acme/lego/v4/providers/dns/beget" - "github.com/go-acme/lego/v4/providers/dns/binarylane" "github.com/go-acme/lego/v4/providers/dns/bindman" "github.com/go-acme/lego/v4/providers/dns/bluecat" - "github.com/go-acme/lego/v4/providers/dns/bluecatv2" - "github.com/go-acme/lego/v4/providers/dns/bookmyname" "github.com/go-acme/lego/v4/providers/dns/brandit" "github.com/go-acme/lego/v4/providers/dns/bunny" "github.com/go-acme/lego/v4/providers/dns/checkdomain" @@ -37,20 +24,13 @@ import ( "github.com/go-acme/lego/v4/providers/dns/cloudns" "github.com/go-acme/lego/v4/providers/dns/cloudru" "github.com/go-acme/lego/v4/providers/dns/cloudxns" - "github.com/go-acme/lego/v4/providers/dns/com35" "github.com/go-acme/lego/v4/providers/dns/conoha" - "github.com/go-acme/lego/v4/providers/dns/conohav3" "github.com/go-acme/lego/v4/providers/dns/constellix" - "github.com/go-acme/lego/v4/providers/dns/corenetworks" "github.com/go-acme/lego/v4/providers/dns/cpanel" - "github.com/go-acme/lego/v4/providers/dns/czechia" - "github.com/go-acme/lego/v4/providers/dns/ddnss" "github.com/go-acme/lego/v4/providers/dns/derak" "github.com/go-acme/lego/v4/providers/dns/desec" "github.com/go-acme/lego/v4/providers/dns/designate" "github.com/go-acme/lego/v4/providers/dns/digitalocean" - "github.com/go-acme/lego/v4/providers/dns/directadmin" - "github.com/go-acme/lego/v4/providers/dns/dnsexit" "github.com/go-acme/lego/v4/providers/dns/dnshomede" "github.com/go-acme/lego/v4/providers/dns/dnsimple" "github.com/go-acme/lego/v4/providers/dns/dnsmadeeasy" @@ -60,37 +40,26 @@ import ( "github.com/go-acme/lego/v4/providers/dns/dreamhost" "github.com/go-acme/lego/v4/providers/dns/duckdns" "github.com/go-acme/lego/v4/providers/dns/dyn" - "github.com/go-acme/lego/v4/providers/dns/dyndnsfree" "github.com/go-acme/lego/v4/providers/dns/dynu" "github.com/go-acme/lego/v4/providers/dns/easydns" - "github.com/go-acme/lego/v4/providers/dns/edgecenter" "github.com/go-acme/lego/v4/providers/dns/edgedns" - "github.com/go-acme/lego/v4/providers/dns/edgeone" "github.com/go-acme/lego/v4/providers/dns/efficientip" "github.com/go-acme/lego/v4/providers/dns/epik" - "github.com/go-acme/lego/v4/providers/dns/eurodns" - "github.com/go-acme/lego/v4/providers/dns/excedo" "github.com/go-acme/lego/v4/providers/dns/exec" "github.com/go-acme/lego/v4/providers/dns/exoscale" - "github.com/go-acme/lego/v4/providers/dns/f5xc" "github.com/go-acme/lego/v4/providers/dns/freemyip" "github.com/go-acme/lego/v4/providers/dns/gandi" "github.com/go-acme/lego/v4/providers/dns/gandiv5" "github.com/go-acme/lego/v4/providers/dns/gcloud" "github.com/go-acme/lego/v4/providers/dns/gcore" - "github.com/go-acme/lego/v4/providers/dns/gigahostno" "github.com/go-acme/lego/v4/providers/dns/glesys" "github.com/go-acme/lego/v4/providers/dns/godaddy" "github.com/go-acme/lego/v4/providers/dns/googledomains" - "github.com/go-acme/lego/v4/providers/dns/gravity" "github.com/go-acme/lego/v4/providers/dns/hetzner" "github.com/go-acme/lego/v4/providers/dns/hostingde" - "github.com/go-acme/lego/v4/providers/dns/hostinger" - "github.com/go-acme/lego/v4/providers/dns/hostingnl" "github.com/go-acme/lego/v4/providers/dns/hosttech" "github.com/go-acme/lego/v4/providers/dns/httpnet" "github.com/go-acme/lego/v4/providers/dns/httpreq" - "github.com/go-acme/lego/v4/providers/dns/huaweicloud" "github.com/go-acme/lego/v4/providers/dns/hurricane" "github.com/go-acme/lego/v4/providers/dns/hyperone" "github.com/go-acme/lego/v4/providers/dns/ibmcloud" @@ -101,47 +70,30 @@ import ( "github.com/go-acme/lego/v4/providers/dns/internetbs" "github.com/go-acme/lego/v4/providers/dns/inwx" "github.com/go-acme/lego/v4/providers/dns/ionos" - "github.com/go-acme/lego/v4/providers/dns/ionoscloud" "github.com/go-acme/lego/v4/providers/dns/ipv64" - "github.com/go-acme/lego/v4/providers/dns/ispconfig" - "github.com/go-acme/lego/v4/providers/dns/ispconfigddns" "github.com/go-acme/lego/v4/providers/dns/iwantmyname" - "github.com/go-acme/lego/v4/providers/dns/jdcloud" "github.com/go-acme/lego/v4/providers/dns/joker" - "github.com/go-acme/lego/v4/providers/dns/keyhelp" - "github.com/go-acme/lego/v4/providers/dns/leaseweb" "github.com/go-acme/lego/v4/providers/dns/liara" "github.com/go-acme/lego/v4/providers/dns/lightsail" - "github.com/go-acme/lego/v4/providers/dns/limacity" "github.com/go-acme/lego/v4/providers/dns/linode" "github.com/go-acme/lego/v4/providers/dns/liquidweb" "github.com/go-acme/lego/v4/providers/dns/loopia" "github.com/go-acme/lego/v4/providers/dns/luadns" "github.com/go-acme/lego/v4/providers/dns/mailinabox" - "github.com/go-acme/lego/v4/providers/dns/manageengine" - "github.com/go-acme/lego/v4/providers/dns/manual" "github.com/go-acme/lego/v4/providers/dns/metaname" - "github.com/go-acme/lego/v4/providers/dns/metaregistrar" - "github.com/go-acme/lego/v4/providers/dns/mijnhost" - "github.com/go-acme/lego/v4/providers/dns/mittwald" - "github.com/go-acme/lego/v4/providers/dns/myaddr" "github.com/go-acme/lego/v4/providers/dns/mydnsjp" "github.com/go-acme/lego/v4/providers/dns/mythicbeasts" "github.com/go-acme/lego/v4/providers/dns/namecheap" "github.com/go-acme/lego/v4/providers/dns/namedotcom" "github.com/go-acme/lego/v4/providers/dns/namesilo" - "github.com/go-acme/lego/v4/providers/dns/namesurfer" "github.com/go-acme/lego/v4/providers/dns/nearlyfreespeech" - "github.com/go-acme/lego/v4/providers/dns/neodigit" "github.com/go-acme/lego/v4/providers/dns/netcup" "github.com/go-acme/lego/v4/providers/dns/netlify" "github.com/go-acme/lego/v4/providers/dns/nicmanager" - "github.com/go-acme/lego/v4/providers/dns/nicru" "github.com/go-acme/lego/v4/providers/dns/nifcloud" "github.com/go-acme/lego/v4/providers/dns/njalla" "github.com/go-acme/lego/v4/providers/dns/nodion" "github.com/go-acme/lego/v4/providers/dns/ns1" - "github.com/go-acme/lego/v4/providers/dns/octenium" "github.com/go-acme/lego/v4/providers/dns/oraclecloud" "github.com/go-acme/lego/v4/providers/dns/otc" "github.com/go-acme/lego/v4/providers/dns/ovh" @@ -149,9 +101,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/plesk" "github.com/go-acme/lego/v4/providers/dns/porkbun" "github.com/go-acme/lego/v4/providers/dns/rackspace" - "github.com/go-acme/lego/v4/providers/dns/rainyun" "github.com/go-acme/lego/v4/providers/dns/rcodezero" - "github.com/go-acme/lego/v4/providers/dns/regfish" "github.com/go-acme/lego/v4/providers/dns/regru" "github.com/go-acme/lego/v4/providers/dns/rfc2136" "github.com/go-acme/lego/v4/providers/dns/rimuhosting" @@ -161,40 +111,28 @@ import ( "github.com/go-acme/lego/v4/providers/dns/scaleway" "github.com/go-acme/lego/v4/providers/dns/selectel" "github.com/go-acme/lego/v4/providers/dns/selectelv2" - "github.com/go-acme/lego/v4/providers/dns/selfhostde" "github.com/go-acme/lego/v4/providers/dns/servercow" "github.com/go-acme/lego/v4/providers/dns/shellrent" "github.com/go-acme/lego/v4/providers/dns/simply" "github.com/go-acme/lego/v4/providers/dns/sonic" - "github.com/go-acme/lego/v4/providers/dns/spaceship" "github.com/go-acme/lego/v4/providers/dns/stackpath" - "github.com/go-acme/lego/v4/providers/dns/syse" - "github.com/go-acme/lego/v4/providers/dns/technitium" "github.com/go-acme/lego/v4/providers/dns/tencentcloud" - "github.com/go-acme/lego/v4/providers/dns/timewebcloud" - "github.com/go-acme/lego/v4/providers/dns/todaynic" "github.com/go-acme/lego/v4/providers/dns/transip" "github.com/go-acme/lego/v4/providers/dns/ultradns" - "github.com/go-acme/lego/v4/providers/dns/uniteddomains" "github.com/go-acme/lego/v4/providers/dns/variomedia" "github.com/go-acme/lego/v4/providers/dns/vegadns" "github.com/go-acme/lego/v4/providers/dns/vercel" "github.com/go-acme/lego/v4/providers/dns/versio" "github.com/go-acme/lego/v4/providers/dns/vinyldns" - "github.com/go-acme/lego/v4/providers/dns/virtualname" "github.com/go-acme/lego/v4/providers/dns/vkcloud" - "github.com/go-acme/lego/v4/providers/dns/volcengine" "github.com/go-acme/lego/v4/providers/dns/vscale" "github.com/go-acme/lego/v4/providers/dns/vultr" "github.com/go-acme/lego/v4/providers/dns/webnames" - "github.com/go-acme/lego/v4/providers/dns/webnamesca" "github.com/go-acme/lego/v4/providers/dns/websupport" "github.com/go-acme/lego/v4/providers/dns/wedos" - "github.com/go-acme/lego/v4/providers/dns/westcn" "github.com/go-acme/lego/v4/providers/dns/yandex" "github.com/go-acme/lego/v4/providers/dns/yandex360" "github.com/go-acme/lego/v4/providers/dns/yandexcloud" - "github.com/go-acme/lego/v4/providers/dns/zoneedit" "github.com/go-acme/lego/v4/providers/dns/zoneee" "github.com/go-acme/lego/v4/providers/dns/zonomi" ) @@ -202,50 +140,26 @@ import ( // NewDNSChallengeProviderByName Factory for DNS providers. func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { switch name { - case "acme-dns", "acmedns": + case "acme-dns": // TODO(ldez): remove "-" in v5 return acmedns.NewDNSProvider() - case "active24": - return active24.NewDNSProvider() case "alidns": return alidns.NewDNSProvider() - case "aliesa": - return aliesa.NewDNSProvider() case "allinkl": return allinkl.NewDNSProvider() - case "alwaysdata": - return alwaysdata.NewDNSProvider() - case "anexia": - return anexia.NewDNSProvider() - case "artfiles": - return artfiles.NewDNSProvider() case "arvancloud": return arvancloud.NewDNSProvider() - case "auroradns": - return auroradns.NewDNSProvider() - case "autodns": - return autodns.NewDNSProvider() - case "axelname": - return axelname.NewDNSProvider() - case "azion": - return azion.NewDNSProvider() case "azure": return azure.NewDNSProvider() case "azuredns": return azuredns.NewDNSProvider() - case "baiducloud": - return baiducloud.NewDNSProvider() - case "beget": - return beget.NewDNSProvider() - case "binarylane": - return binarylane.NewDNSProvider() + case "auroradns": + return auroradns.NewDNSProvider() + case "autodns": + return autodns.NewDNSProvider() case "bindman": return bindman.NewDNSProvider() case "bluecat": return bluecat.NewDNSProvider() - case "bluecatv2": - return bluecatv2.NewDNSProvider() - case "bookmyname": - return bookmyname.NewDNSProvider() case "brandit": return brandit.NewDNSProvider() case "bunny": @@ -264,22 +178,12 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return cloudru.NewDNSProvider() case "cloudxns": return cloudxns.NewDNSProvider() - case "com35": - return com35.NewDNSProvider() case "conoha": return conoha.NewDNSProvider() - case "conohav3": - return conohav3.NewDNSProvider() case "constellix": return constellix.NewDNSProvider() - case "corenetworks": - return corenetworks.NewDNSProvider() case "cpanel": return cpanel.NewDNSProvider() - case "czechia": - return czechia.NewDNSProvider() - case "ddnss": - return ddnss.NewDNSProvider() case "derak": return derak.NewDNSProvider() case "desec": @@ -288,10 +192,6 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return designate.NewDNSProvider() case "digitalocean": return digitalocean.NewDNSProvider() - case "directadmin": - return directadmin.NewDNSProvider() - case "dnsexit": - return dnsexit.NewDNSProvider() case "dnshomede": return dnshomede.NewDNSProvider() case "dnsimple": @@ -310,32 +210,20 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return duckdns.NewDNSProvider() case "dyn": return dyn.NewDNSProvider() - case "dyndnsfree": - return dyndnsfree.NewDNSProvider() case "dynu": return dynu.NewDNSProvider() case "easydns": return easydns.NewDNSProvider() - case "edgecenter": - return edgecenter.NewDNSProvider() - case "edgedns", "fastdns": + case "edgedns", "fastdns": // "fastdns" is for compatibility with v3, must be dropped in v5 return edgedns.NewDNSProvider() - case "edgeone": - return edgeone.NewDNSProvider() case "efficientip": return efficientip.NewDNSProvider() case "epik": return epik.NewDNSProvider() - case "eurodns": - return eurodns.NewDNSProvider() - case "excedo": - return excedo.NewDNSProvider() case "exec": return exec.NewDNSProvider() case "exoscale": return exoscale.NewDNSProvider() - case "f5xc": - return f5xc.NewDNSProvider() case "freemyip": return freemyip.NewDNSProvider() case "gandi": @@ -346,32 +234,22 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return gcloud.NewDNSProvider() case "gcore": return gcore.NewDNSProvider() - case "gigahostno": - return gigahostno.NewDNSProvider() case "glesys": return glesys.NewDNSProvider() case "godaddy": return godaddy.NewDNSProvider() case "googledomains": return googledomains.NewDNSProvider() - case "gravity": - return gravity.NewDNSProvider() case "hetzner": return hetzner.NewDNSProvider() case "hostingde": return hostingde.NewDNSProvider() - case "hostinger": - return hostinger.NewDNSProvider() - case "hostingnl": - return hostingnl.NewDNSProvider() case "hosttech": return hosttech.NewDNSProvider() case "httpnet": return httpnet.NewDNSProvider() case "httpreq": return httpreq.NewDNSProvider() - case "huaweicloud": - return huaweicloud.NewDNSProvider() case "hurricane": return hurricane.NewDNSProvider() case "hyperone": @@ -392,31 +270,17 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return inwx.NewDNSProvider() case "ionos": return ionos.NewDNSProvider() - case "ionoscloud": - return ionoscloud.NewDNSProvider() case "ipv64": return ipv64.NewDNSProvider() - case "ispconfig": - return ispconfig.NewDNSProvider() - case "ispconfigddns": - return ispconfigddns.NewDNSProvider() case "iwantmyname": return iwantmyname.NewDNSProvider() - case "jdcloud": - return jdcloud.NewDNSProvider() case "joker": return joker.NewDNSProvider() - case "keyhelp": - return keyhelp.NewDNSProvider() - case "leaseweb": - return leaseweb.NewDNSProvider() case "liara": return liara.NewDNSProvider() case "lightsail": return lightsail.NewDNSProvider() - case "limacity": - return limacity.NewDNSProvider() - case "linode", "linodev4": + case "linode", "linodev4": // "linodev4" is for compatibility with v3, must be dropped in v5 return linode.NewDNSProvider() case "liquidweb": return liquidweb.NewDNSProvider() @@ -426,20 +290,10 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return luadns.NewDNSProvider() case "mailinabox": return mailinabox.NewDNSProvider() - case "manageengine": - return manageengine.NewDNSProvider() case "manual": - return manual.NewDNSProvider() + return dns01.NewDNSProviderManual() case "metaname": return metaname.NewDNSProvider() - case "metaregistrar": - return metaregistrar.NewDNSProvider() - case "mijnhost": - return mijnhost.NewDNSProvider() - case "mittwald": - return mittwald.NewDNSProvider() - case "myaddr": - return myaddr.NewDNSProvider() case "mydnsjp": return mydnsjp.NewDNSProvider() case "mythicbeasts": @@ -450,20 +304,14 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return namedotcom.NewDNSProvider() case "namesilo": return namesilo.NewDNSProvider() - case "namesurfer": - return namesurfer.NewDNSProvider() case "nearlyfreespeech": return nearlyfreespeech.NewDNSProvider() - case "neodigit": - return neodigit.NewDNSProvider() case "netcup": return netcup.NewDNSProvider() case "netlify": return netlify.NewDNSProvider() case "nicmanager": return nicmanager.NewDNSProvider() - case "nicru": - return nicru.NewDNSProvider() case "nifcloud": return nifcloud.NewDNSProvider() case "njalla": @@ -472,8 +320,6 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return nodion.NewDNSProvider() case "ns1": return ns1.NewDNSProvider() - case "octenium": - return octenium.NewDNSProvider() case "oraclecloud": return oraclecloud.NewDNSProvider() case "otc": @@ -488,12 +334,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return porkbun.NewDNSProvider() case "rackspace": return rackspace.NewDNSProvider() - case "rainyun": - return rainyun.NewDNSProvider() case "rcodezero": return rcodezero.NewDNSProvider() - case "regfish": - return regfish.NewDNSProvider() case "regru": return regru.NewDNSProvider() case "rfc2136": @@ -512,8 +354,6 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return selectel.NewDNSProvider() case "selectelv2": return selectelv2.NewDNSProvider() - case "selfhostde": - return selfhostde.NewDNSProvider() case "servercow": return servercow.NewDNSProvider() case "shellrent": @@ -522,26 +362,14 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return simply.NewDNSProvider() case "sonic": return sonic.NewDNSProvider() - case "spaceship": - return spaceship.NewDNSProvider() case "stackpath": return stackpath.NewDNSProvider() - case "syse": - return syse.NewDNSProvider() - case "technitium": - return technitium.NewDNSProvider() case "tencentcloud": return tencentcloud.NewDNSProvider() - case "timewebcloud": - return timewebcloud.NewDNSProvider() - case "todaynic": - return todaynic.NewDNSProvider() case "transip": return transip.NewDNSProvider() case "ultradns": return ultradns.NewDNSProvider() - case "uniteddomains": - return uniteddomains.NewDNSProvider() case "variomedia": return variomedia.NewDNSProvider() case "vegadns": @@ -552,34 +380,24 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return versio.NewDNSProvider() case "vinyldns": return vinyldns.NewDNSProvider() - case "virtualname": - return virtualname.NewDNSProvider() case "vkcloud": return vkcloud.NewDNSProvider() - case "volcengine": - return volcengine.NewDNSProvider() case "vscale": return vscale.NewDNSProvider() case "vultr": return vultr.NewDNSProvider() - case "webnames", "webnamesru": + case "webnames": return webnames.NewDNSProvider() - case "webnamesca": - return webnamesca.NewDNSProvider() case "websupport": return websupport.NewDNSProvider() case "wedos": return wedos.NewDNSProvider() - case "westcn": - return westcn.NewDNSProvider() case "yandex": return yandex.NewDNSProvider() case "yandex360": return yandex360.NewDNSProvider() case "yandexcloud": return yandexcloud.NewDNSProvider() - case "zoneedit": - return zoneedit.NewDNSProvider() case "zoneee": return zoneee.NewDNSProvider() case "zonomi": diff --git a/providers/dns/dns_providers_test.go b/providers/dns/dns_providers_test.go index 3b82784b4..1f39e2bdd 100644 --- a/providers/dns/dns_providers_test.go +++ b/providers/dns/dns_providers_test.go @@ -13,7 +13,6 @@ var envTest = tester.NewEnvTest("EXEC_PATH") func TestKnownDNSProviderSuccess(t *testing.T) { defer envTest.RestoreEnv() - envTest.Apply(map[string]string{ "EXEC_PATH": "abc", }) @@ -27,7 +26,6 @@ func TestKnownDNSProviderSuccess(t *testing.T) { func TestKnownDNSProviderError(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() provider, err := NewDNSChallengeProviderByName("exec") diff --git a/providers/dns/dnsexit/dnsexit.go b/providers/dns/dnsexit/dnsexit.go deleted file mode 100644 index ce9373a50..000000000 --- a/providers/dns/dnsexit/dnsexit.go +++ /dev/null @@ -1,163 +0,0 @@ -// Package dnsexit implements a DNS provider for solving the DNS-01 challenge using DNSExit. -package dnsexit - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/dnsexit/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "DNSEXIT_" - - EnvAPIKey = envNamespace + "API_KEY" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for DNSExit. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIKey) - if err != nil { - return nil, fmt.Errorf("dnsexit: %w", err) - } - - config := NewDefaultConfig() - config.APIKey = values[EnvAPIKey] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for DNSExit. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("dnsexit: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.APIKey) - if err != nil { - return nil, fmt.Errorf("dnsexit: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("dnsexit: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("dnsexit: %w", err) - } - - record := internal.Record{ - Type: "TXT", - Name: subDomain, - Content: info.Value, - TTL: toMinutes(d.config.TTL), - } - - err = d.client.AddRecord(context.Background(), dns01.UnFqdn(authZone), record) - if err != nil { - return fmt.Errorf("dnsexit: add record: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("dnsexit: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("dnsexit: %w", err) - } - - record := internal.Record{ - Type: "TXT", - Name: subDomain, - Content: info.Value, - } - - err = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), record) - if err != nil { - return fmt.Errorf("dnsexit: add record: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -func toMinutes(seconds int) int { - i := seconds / 60 - if seconds%60 > 0 { - i++ - } - - return i -} diff --git a/providers/dns/dnsexit/dnsexit.toml b/providers/dns/dnsexit/dnsexit.toml deleted file mode 100644 index 0d5321835..000000000 --- a/providers/dns/dnsexit/dnsexit.toml +++ /dev/null @@ -1,22 +0,0 @@ -Name = "DNSExit" -Description = '''''' -URL = "https://dnsexit.com" -Code = "dnsexit" -Since = "v4.32.0" - -Example = ''' -DNSEXIT_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns dnsexit -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - DNSEXIT_API_KEY = "API key" - [Configuration.Additional] - DNSEXIT_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" - DNSEXIT_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 300)" - DNSEXIT_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - DNSEXIT_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://dnsexit.com/dns/dns-api/" diff --git a/providers/dns/dnsexit/dnsexit_test.go b/providers/dns/dnsexit/dnsexit_test.go deleted file mode 100644 index 31fe61497..000000000 --- a/providers/dns/dnsexit/dnsexit_test.go +++ /dev/null @@ -1,165 +0,0 @@ -package dnsexit - -import ( - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAPIKey: "key", - }, - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "dnsexit: some credentials information are missing: DNSEXIT_API_KEY", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiKey string - expected string - }{ - { - desc: "success", - apiKey: "key", - }, - { - desc: "missing credentials", - expected: "dnsexit: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIKey = test.apiKey - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIKey = "secret" - config.HTTPClient = server.Client() - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - p.client.BaseURL, _ = url.Parse(server.URL) - - return p, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(). - With("apikey", "secret"), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("POST /", - servermock.ResponseFromInternal("success.json"), - servermock.CheckRequestJSONBodyFromInternal("add_record-request.json"), - ). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("POST /", - servermock.ResponseFromInternal("success.json"), - servermock.CheckRequestJSONBodyFromInternal("delete_record-request.json"), - ). - Build(t) - - err := provider.CleanUp("example.com", "abc", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/dnsexit/internal/client.go b/providers/dns/dnsexit/internal/client.go deleted file mode 100644 index 9b0164846..000000000 --- a/providers/dns/dnsexit/internal/client.go +++ /dev/null @@ -1,156 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" -) - -const defaultBaseURL = "https://api.dnsexit.com/dns/" - -// Client the DNSExit API client. -type Client struct { - apiKey string - - BaseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(apiKey string) (*Client, error) { - if apiKey == "" { - return nil, errors.New("credentials missing") - } - - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - apiKey: apiKey, - BaseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -// AddRecord adds a record. -// https://dnsexit.com/dns/dns-api/#example-add-spf -// https://dnsexit.com/dns/dns-api/#example-lse -func (c *Client) AddRecord(ctx context.Context, domain string, record Record) error { - payload := APIRequest{ - Domain: domain, - Add: []Record{record}, - } - - req, err := newJSONRequest(ctx, http.MethodPost, c.BaseURL, payload) - if err != nil { - return err - } - - err = c.do(req) - if err != nil { - return err - } - - return nil -} - -// DeleteRecord deletes a record. -// https://dnsexit.com/dns/dns-api/#delete-a-record -func (c *Client) DeleteRecord(ctx context.Context, domain string, record Record) error { - payload := APIRequest{ - Domain: domain, - Delete: []Record{record}, - } - - req, err := newJSONRequest(ctx, http.MethodPost, c.BaseURL, payload) - if err != nil { - return err - } - - err = c.do(req) - if err != nil { - return err - } - - return nil -} - -func (c *Client) do(req *http.Request) error { - useragent.SetHeader(req.Header) - - req.Header.Set("apikey", c.apiKey) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode > http.StatusBadRequest { - return parseError(req, resp) - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - result := &APIResponse{} - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - if result.Code != 0 { - return result - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIResponse - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return &errAPI -} diff --git a/providers/dns/dnsexit/internal/client_test.go b/providers/dns/dnsexit/internal/client_test.go deleted file mode 100644 index 26ea01203..000000000 --- a/providers/dns/dnsexit/internal/client_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package internal - -import ( - "context" - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("secret") - if err != nil { - return nil, err - } - - client.HTTPClient = server.Client() - client.BaseURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(). - With("apikey", "secret"), - ) -} - -func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /", - servermock.ResponseFromFixture("success.json"), - servermock.CheckRequestJSONBodyFromFixture("add_record-request.json"), - ). - Build(t) - - record := Record{ - Type: "TXT", - Name: "_acme-challenge", - Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: 2, - } - - err := client.AddRecord(context.Background(), "example.com", record) - require.NoError(t, err) -} - -func TestClient_AddRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusBadRequest), - ). - Build(t) - - record := Record{ - Type: "TXT", - Name: "_acme-challenge", - Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: 480, - Overwrite: true, - } - - err := client.AddRecord(context.Background(), "example.com", record) - require.Error(t, err) - - require.EqualError(t, err, "JSON Defined Record Type not Supported (code=6)") -} - -func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /", - servermock.ResponseFromFixture("success.json"), - servermock.CheckRequestJSONBodyFromFixture("delete_record-request.json"), - ). - Build(t) - - record := Record{ - Type: "TXT", - Name: "_acme-challenge", - Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - } - - err := client.DeleteRecord(context.Background(), "example.com", record) - require.NoError(t, err) -} - -func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusBadRequest), - ). - Build(t) - - record := Record{ - Type: "TXT", - Name: "foo", - Content: "txtTXTtxt", - } - - err := client.DeleteRecord(context.Background(), "example.com", record) - - require.Error(t, err) - - require.EqualError(t, err, "JSON Defined Record Type not Supported (code=6)") -} diff --git a/providers/dns/dnsexit/internal/fixtures/add_record-request.json b/providers/dns/dnsexit/internal/fixtures/add_record-request.json deleted file mode 100644 index 6e5e2b520..000000000 --- a/providers/dns/dnsexit/internal/fixtures/add_record-request.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "domain": "example.com", - "add": [ - { - "type": "TXT", - "name": "_acme-challenge", - "content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "ttl": 2 - } - ] -} diff --git a/providers/dns/dnsexit/internal/fixtures/delete_record-request.json b/providers/dns/dnsexit/internal/fixtures/delete_record-request.json deleted file mode 100644 index dcfef9cdf..000000000 --- a/providers/dns/dnsexit/internal/fixtures/delete_record-request.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "example.com", - "delete": [ - { - "type": "TXT", - "name": "_acme-challenge", - "content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" - } - ] -} diff --git a/providers/dns/dnsexit/internal/fixtures/error.json b/providers/dns/dnsexit/internal/fixtures/error.json deleted file mode 100644 index 9ba835895..000000000 --- a/providers/dns/dnsexit/internal/fixtures/error.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "code": 6, - "message": "JSON Defined Record Type not Supported" -} diff --git a/providers/dns/dnsexit/internal/fixtures/success.json b/providers/dns/dnsexit/internal/fixtures/success.json deleted file mode 100644 index 3af47a936..000000000 --- a/providers/dns/dnsexit/internal/fixtures/success.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "code": 0, - "details": [ - "UPDATE Record A example.com. TTL(hh:mm) 08:00 IP 1.1.1.10" - ], - "message": "Success" -} diff --git a/providers/dns/dnsexit/internal/types.go b/providers/dns/dnsexit/internal/types.go deleted file mode 100644 index db254549f..000000000 --- a/providers/dns/dnsexit/internal/types.go +++ /dev/null @@ -1,41 +0,0 @@ -package internal - -import ( - "fmt" - "strings" -) - -type Record struct { - Type string `json:"type,omitempty"` - Name string `json:"name,omitempty"` - Content string `json:"content,omitempty"` - TTL int `json:"ttl,omitempty"` // NOTE: ttl value is in minutes. - Overwrite bool `json:"overwrite,omitempty"` -} - -type APIRequest struct { - Domain string `json:"domain,omitempty"` - Add []Record `json:"add,omitempty"` - Delete []Record `json:"delete,omitempty"` - Update []Record `json:"update,omitempty"` -} - -// https://dnsexit.com/dns/dns-api/#server-reply - -type APIResponse struct { - Code int `json:"code,omitempty"` - Details []string `json:"details,omitempty"` - Message string `json:"message,omitempty"` -} - -func (a APIResponse) Error() string { - msg := new(strings.Builder) - - _, _ = fmt.Fprintf(msg, "%s (code=%d)", a.Message, a.Code) - - for _, detail := range a.Details { - _, _ = fmt.Fprintf(msg, ", %s", detail) - } - - return msg.String() -} diff --git a/providers/dns/dnshomede/dnshomede.go b/providers/dns/dnshomede/dnshomede.go index c76ed6de2..1b81be744 100644 --- a/providers/dns/dnshomede/dnshomede.go +++ b/providers/dns/dnshomede/dnshomede.go @@ -6,12 +6,12 @@ import ( "errors" "fmt" "net/http" + "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/dnshomede/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -57,15 +57,14 @@ type DNSProvider struct { // Credentials must be passed in the environment variable: DNSHOMEDE_CREDENTIALS. func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() - values, err := env.Get(EnvCredentials) if err != nil { return nil, fmt.Errorf("dnshomede: %w", err) } - credentials, err := env.ParsePairs(values[EnvCredentials]) + credentials, err := parseCredentials(values[EnvCredentials]) if err != nil { - return nil, fmt.Errorf("dnshomede: credentials: %w", err) + return nil, fmt.Errorf("dnshomede: %w", err) } config.Credentials = credentials @@ -94,12 +93,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client := internal.NewClient(config.Credentials) - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{config: config, client: client}, nil } @@ -138,3 +131,19 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { func (d *DNSProvider) Sequential() time.Duration { return d.config.SequenceInterval } + +func parseCredentials(raw string) (map[string]string, error) { + credentials := make(map[string]string) + + credStrings := strings.Split(strings.TrimSuffix(raw, ","), ",") + for _, credPair := range credStrings { + data := strings.Split(credPair, ":") + if len(data) != 2 { + return nil, fmt.Errorf("invalid credential pair: %q", credPair) + } + + credentials[strings.TrimSpace(data[0])] = strings.TrimSpace(data[1]) + } + + return credentials, nil +} diff --git a/providers/dns/dnshomede/dnshomede.toml b/providers/dns/dnshomede/dnshomede.toml index 9c3b65277..0598f38fb 100644 --- a/providers/dns/dnshomede/dnshomede.toml +++ b/providers/dns/dnshomede/dnshomede.toml @@ -5,18 +5,18 @@ Code = "dnshomede" Since = "v4.10.0" Example = ''' -DNSHOMEDE_CREDENTIALS=example.org:password \ -lego --dns dnshomede -d '*.example.com' -d example.com run +DNSHOMEDE_CREDENTIALS=sub.example.org:password \ +lego --email you@example.com --dns dnshomede --domains example.org --domains '*.example.org' run DNSHOMEDE_CREDENTIALS=my.example.org:password1,demo.example.org:password2 \ -lego --dns dnshomede -d my.example.org -d demo.example.org +lego --email you@example.com --dns dnshomede --domains my.example.org --domains demo.example.org ''' [Configuration] [Configuration.Credentials] DNSHOMEDE_CREDENTIALS = "Comma-separated list of domain:password credential pairs" - [Configuration.Additional] - DNSHOMEDE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 1200)" - DNSHOMEDE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 2)" - DNSHOMEDE_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 120)" - DNSHOMEDE_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + [Configuration.Addtional] + DNSHOMEDE_POLLING_INTERVAL = "Time between DNS propagation checks" + DNSHOMEDE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation; defaults to 300s (5 minutes)" + DNSHOMEDE_SEQUENCE_INTERVAL = "Time between sequential requests" + DNSHOMEDE_HTTP_TIMEOUT = "API request timeout" diff --git a/providers/dns/dnshomede/dnshomede_test.go b/providers/dns/dnshomede/dnshomede_test.go index 5035a7837..6b79912e8 100644 --- a/providers/dns/dnshomede/dnshomede_test.go +++ b/providers/dns/dnshomede/dnshomede_test.go @@ -34,7 +34,7 @@ func TestNewDNSProvider(t *testing.T) { envVars: map[string]string{ EnvCredentials: ",", }, - expected: `dnshomede: credentials: incorrect pair: `, + expected: `dnshomede: invalid credential pair: ""`, }, { desc: "missing password", @@ -55,7 +55,7 @@ func TestNewDNSProvider(t *testing.T) { envVars: map[string]string{ EnvCredentials: "example.org:123,example.net", }, - expected: "dnshomede: credentials: incorrect pair: example.net", + expected: `dnshomede: invalid credential pair: "example.net"`, }, { desc: "missing credentials", @@ -69,7 +69,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -145,7 +144,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -159,7 +157,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/dnshomede/internal/client_test.go b/providers/dns/dnshomede/internal/client_test.go index 6e1593fe7..e6f2c1b7d 100644 --- a/providers/dns/dnshomede/internal/client_test.go +++ b/providers/dns/dnshomede/internal/client_test.go @@ -1,109 +1,89 @@ package internal import ( + "context" "fmt" + "net/http" "net/http/httptest" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/require" ) -func setupClient(credentials map[string]string) func(server *httptest.Server) (*Client, error) { - return func(server *httptest.Server) (*Client, error) { - client := NewClient(credentials) - client.HTTPClient = server.Client() - client.baseURL = server.URL +func setupTest(t *testing.T, credentials map[string]string, handler http.HandlerFunc) *Client { + t.Helper() - return client, nil - } + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc("/", handler) + + client := NewClient(credentials) + client.HTTPClient = server.Client() + client.baseURL = server.URL + + return client } func TestClient_Add(t *testing.T) { txtValue := "123456789012" - client := servermock.NewBuilder[*Client](setupClient(map[string]string{"example.org": "secret"})). - Route("POST /", - servermock.RawStringResponse(fmt.Sprintf("%s %s", successCode, txtValue)), - servermock.CheckQueryParameter().Strict(). - With("acme", addAction).With("txt", txtValue)). - Build(t) + client := setupTest(t, map[string]string{"example.org": "secret"}, handlerMock(addAction, txtValue)) - err := client.Add(t.Context(), "example.org", txtValue) + err := client.Add(context.Background(), "example.org", txtValue) require.NoError(t, err) } func TestClient_Add_error(t *testing.T) { txtValue := "123456789012" - client := servermock.NewBuilder[*Client](setupClient(map[string]string{"example.com": "secret"})). - Route("POST /", - servermock.RawStringResponse(fmt.Sprintf("%s %s", successCode, txtValue)), - servermock.CheckQueryParameter().Strict(). - With("acme", addAction).With("txt", txtValue)). - Build(t) + client := setupTest(t, map[string]string{"example.com": "secret"}, handlerMock(addAction, txtValue)) - err := client.Add(t.Context(), "example.org", txtValue) - - require.EqualError(t, err, "domain example.org not found in credentials, check your credentials map") + err := client.Add(context.Background(), "example.org", txtValue) + require.Error(t, err) } func TestClient_Remove(t *testing.T) { txtValue := "ABCDEFGHIJKL" - client := servermock.NewBuilder[*Client](setupClient(map[string]string{"example.org": "secret"})). - Route("POST /", - servermock.RawStringResponse(fmt.Sprintf("%s %s", successCode, txtValue)), - servermock.CheckQueryParameter().Strict(). - With("acme", removeAction).With("txt", txtValue)). - Build(t) + client := setupTest(t, map[string]string{"example.org": "secret"}, handlerMock(removeAction, txtValue)) - err := client.Remove(t.Context(), "example.org", txtValue) + err := client.Remove(context.Background(), "example.org", txtValue) require.NoError(t, err) } func TestClient_Remove_error(t *testing.T) { txtValue := "ABCDEFGHIJKL" - testCases := []struct { - desc string - hostname string - response string - expected string - }{ - { - desc: "response error - txt", - hostname: "example.com", - response: "error - no valid acme txt record", - expected: "error - no valid acme txt record", - }, - { - desc: "response error - acme", - hostname: "example.com", - response: "nochg 1234:1234:1234:1234:1234:1234:1234:1234", - expected: "nochg 1234:1234:1234:1234:1234:1234:1234:1234", - }, - { - desc: "credential error", - hostname: "example.org", - response: fmt.Sprintf("%s %s", successCode, txtValue), - expected: "domain example.org not found in credentials, check your credentials map", - }, - } + client := setupTest(t, map[string]string{"example.com": "secret"}, handlerMock(removeAction, txtValue)) - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() + err := client.Remove(context.Background(), "example.org", txtValue) + require.Error(t, err) +} - client := servermock.NewBuilder[*Client](setupClient(map[string]string{"example.com": "secret"})). - Route("POST /", - servermock.RawStringResponse(test.response), - servermock.CheckQueryParameter().Strict(). - With("acme", removeAction).With("txt", txtValue)). - Build(t) +func handlerMock(action, value string) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) - err := client.Remove(t.Context(), test.hostname, txtValue) - require.EqualError(t, err, test.expected) - }) + query := req.URL.Query() + + if query.Get("acme") != action { + _, _ = rw.Write([]byte("nochg 1234:1234:1234:1234:1234:1234:1234:1234")) + return + } + + txtValue := query.Get("txt") + if len(txtValue) < 12 { + _, _ = rw.Write([]byte("error - no valid acme txt record")) + return + } + + if txtValue != value { + http.Error(rw, fmt.Sprintf("got: %q, expected: %q", txtValue, value), http.StatusBadRequest) + return + } + + _, _ = fmt.Fprintf(rw, "%s %s", successCode, txtValue) } } diff --git a/providers/dns/dnshomede/internal/readme.md b/providers/dns/dnshomede/internal/readme.md index 622c4354d..014b062a1 100644 --- a/providers/dns/dnshomede/internal/readme.md +++ b/providers/dns/dnshomede/internal/readme.md @@ -16,7 +16,7 @@ Always returns StatusOK (200) If the API call works the first word of the response body is `successfully`. -If an error occurs the response body is `error - `. +If an error encoured the response body is `error - `. Can be a POST or a GET. @@ -35,6 +35,6 @@ Always returns StatusOK (200) If the API call works the first word of the response body is `successfully`. -If an error occurs the response body is `error - `. +If an error encoured the response body is `error - `. Can be a POST or a GET. diff --git a/providers/dns/dnsimple/dnsimple.go b/providers/dns/dnsimple/dnsimple.go index adf7d48e2..4a5b87889 100644 --- a/providers/dns/dnsimple/dnsimple.go +++ b/providers/dns/dnsimple/dnsimple.go @@ -8,12 +8,9 @@ import ( "strconv" "time" - "github.com/dnsimple/dnsimple-go/v4/dnsimple" - "github.com/go-acme/lego/v4/challenge" + "github.com/dnsimple/dnsimple-go/dnsimple" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" "golang.org/x/oauth2" ) @@ -30,8 +27,6 @@ const ( EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Debug bool @@ -80,15 +75,9 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("dnsimple: OAuth token is missing") } - client := dnsimple.NewClient( - clientdebug.Wrap( - oauth2.NewClient( - context.Background(), - oauth2.StaticTokenSource(&oauth2.Token{AccessToken: config.AccessToken}), - ), - ), - ) - client.SetUserAgent(useragent.Get()) + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: config.AccessToken}) + client := dnsimple.NewClient(oauth2.NewClient(context.Background(), ts)) + client.SetUserAgent("go-acme/lego") if config.BaseURL != "" { client.BaseURL = config.BaseURL @@ -101,16 +90,14 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) - zoneName, err := d.getHostedZone(ctx, info.EffectiveFQDN) + zoneName, err := d.getHostedZone(info.EffectiveFQDN) if err != nil { return fmt.Errorf("dnsimple: %w", err) } - accountID, err := d.getAccountID(ctx) + accountID, err := d.getAccountID() if err != nil { return fmt.Errorf("dnsimple: %w", err) } @@ -120,7 +107,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("dnsimple: %w", err) } - _, err = d.client.Zones.CreateRecord(ctx, accountID, zoneName, recordAttributes) + _, err = d.client.Zones.CreateRecord(context.Background(), accountID, zoneName, recordAttributes) if err != nil { return fmt.Errorf("dnsimple: API call failed: %w", err) } @@ -130,24 +117,21 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) - records, err := d.findTxtRecords(ctx, info.EffectiveFQDN) + records, err := d.findTxtRecords(info.EffectiveFQDN) if err != nil { return fmt.Errorf("dnsimple: %w", err) } - accountID, err := d.getAccountID(ctx) + accountID, err := d.getAccountID() if err != nil { return fmt.Errorf("dnsimple: %w", err) } var lastErr error - for _, rec := range records { - _, err := d.client.Zones.DeleteRecord(ctx, accountID, rec.ZoneID, rec.ID) + _, err := d.client.Zones.DeleteRecord(context.Background(), accountID, rec.ZoneID, rec.ID) if err != nil { lastErr = fmt.Errorf("dnsimple: %w", err) } @@ -162,36 +146,45 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } -func (d *DNSProvider) getHostedZone(ctx context.Context, domain string) (string, error) { +func (d *DNSProvider) getHostedZone(domain string) (string, error) { authZone, err := dns01.FindZoneByFqdn(domain) if err != nil { return "", fmt.Errorf("could not find zone for FQDN %q: %w", domain, err) } - accountID, err := d.getAccountID(ctx) + accountID, err := d.getAccountID() if err != nil { return "", err } - hostedZone, err := d.client.Zones.GetZone(ctx, accountID, dns01.UnFqdn(authZone)) + zoneName := dns01.UnFqdn(authZone) + + zones, err := d.client.Zones.ListZones(context.Background(), accountID, &dnsimple.ZoneListOptions{NameLike: &zoneName}) if err != nil { - return "", fmt.Errorf("get zone: %w", err) + return "", fmt.Errorf("API call failed: %w", err) } - if hostedZone == nil || hostedZone.Data == nil || hostedZone.Data.ID == 0 { + var hostedZone dnsimple.Zone + for _, zone := range zones.Data { + if zone.Name == zoneName { + hostedZone = zone + } + } + + if hostedZone.ID == 0 { return "", fmt.Errorf("zone %s not found in DNSimple for domain %s", authZone, domain) } - return hostedZone.Data.Name, nil + return hostedZone.Name, nil } -func (d *DNSProvider) findTxtRecords(ctx context.Context, fqdn string) ([]dnsimple.ZoneRecord, error) { - zoneName, err := d.getHostedZone(ctx, fqdn) +func (d *DNSProvider) findTxtRecords(fqdn string) ([]dnsimple.ZoneRecord, error) { + zoneName, err := d.getHostedZone(fqdn) if err != nil { return nil, err } - accountID, err := d.getAccountID(ctx) + accountID, err := d.getAccountID() if err != nil { return nil, err } @@ -201,7 +194,7 @@ func (d *DNSProvider) findTxtRecords(ctx context.Context, fqdn string) ([]dnsimp return nil, err } - result, err := d.client.Zones.ListRecords(ctx, accountID, zoneName, &dnsimple.ZoneRecordListOptions{Name: &subDomain, Type: dnsimple.String("TXT"), ListOptions: dnsimple.ListOptions{}}) + result, err := d.client.Zones.ListRecords(context.Background(), accountID, zoneName, &dnsimple.ZoneRecordListOptions{Name: &subDomain, Type: dnsimple.String("TXT"), ListOptions: dnsimple.ListOptions{}}) if err != nil { return nil, fmt.Errorf("API call has failed: %w", err) } @@ -223,8 +216,8 @@ func newTxtRecord(zoneName, fqdn, value string, ttl int) (dnsimple.ZoneRecordAtt }, nil } -func (d *DNSProvider) getAccountID(ctx context.Context) (string, error) { - whoamiResponse, err := d.client.Identity.Whoami(ctx) +func (d *DNSProvider) getAccountID() (string, error) { + whoamiResponse, err := d.client.Identity.Whoami(context.Background()) if err != nil { return "", err } diff --git a/providers/dns/dnsimple/dnsimple.toml b/providers/dns/dnsimple/dnsimple.toml index 158fb7011..0dd8f06e9 100644 --- a/providers/dns/dnsimple/dnsimple.toml +++ b/providers/dns/dnsimple/dnsimple.toml @@ -6,7 +6,7 @@ Since = "v0.3.0" Example = ''' DNSIMPLE_OAUTH_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \ -lego --dns dnsimple -d '*.example.com' -d example.com run +lego --email you@example.com --dns dnsimple --domains my.example.org run ''' Additional = ''' @@ -32,9 +32,9 @@ Only Account API tokens are supported, if you try to use a User API token you wi DNSIMPLE_OAUTH_TOKEN = "OAuth token" [Configuration.Additional] DNSIMPLE_BASE_URL = "API endpoint URL" - DNSIMPLE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - DNSIMPLE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - DNSIMPLE_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + DNSIMPLE_POLLING_INTERVAL = "Time between DNS propagation check" + DNSIMPLE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + DNSIMPLE_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://developer.dnsimple.com/v2/" diff --git a/providers/dns/dnsimple/dnsimple_test.go b/providers/dns/dnsimple/dnsimple_test.go index 2a52dd2de..c07f965b4 100644 --- a/providers/dns/dnsimple/dnsimple_test.go +++ b/providers/dns/dnsimple/dnsimple_test.go @@ -51,7 +51,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) diff --git a/providers/dns/dnsmadeeasy/dnsmadeeasy.go b/providers/dns/dnsmadeeasy/dnsmadeeasy.go index 69f2096fb..7f4ca2af3 100644 --- a/providers/dns/dnsmadeeasy/dnsmadeeasy.go +++ b/providers/dns/dnsmadeeasy/dnsmadeeasy.go @@ -11,11 +11,9 @@ import ( "strings" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/dnsmadeeasy/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -32,8 +30,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string @@ -48,22 +44,15 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { - tr := &http.Transport{} - - defaultTransport, ok := http.DefaultTransport.(*http.Transport) - if ok { - tr = defaultTransport.Clone() - } - - tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), - Transport: tr, + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, }, } } @@ -113,12 +102,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, fmt.Errorf("dnsmadeeasy: %w", err) } - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - + client.HTTPClient = config.HTTPClient client.BaseURL, err = url.Parse(baseURL) if err != nil { return nil, err @@ -155,7 +139,6 @@ func (d *DNSProvider) Present(domainName, token, keyAuth string) error { if err != nil { return fmt.Errorf("dnsmadeeasy: unable to create record for %s: %w", name, err) } - return nil } @@ -178,7 +161,6 @@ func (d *DNSProvider) CleanUp(domainName, token, keyAuth string) error { // find matching records name := strings.Replace(info.EffectiveFQDN, "."+authZone, "", 1) - records, err := d.client.GetRecords(ctx, domain, name, "TXT") if err != nil { return fmt.Errorf("dnsmadeeasy: unable to get records for domain %s: %w", domain.Name, err) @@ -186,7 +168,6 @@ func (d *DNSProvider) CleanUp(domainName, token, keyAuth string) error { // delete records var lastError error - for _, record := range *records { err = d.client.DeleteRecord(ctx, record) if err != nil { diff --git a/providers/dns/dnsmadeeasy/dnsmadeeasy.toml b/providers/dns/dnsmadeeasy/dnsmadeeasy.toml index d71ab5303..fd0866f56 100644 --- a/providers/dns/dnsmadeeasy/dnsmadeeasy.toml +++ b/providers/dns/dnsmadeeasy/dnsmadeeasy.toml @@ -7,7 +7,7 @@ Since = "v0.4.0" Example = ''' DNSMADEEASY_API_KEY=xxxxxx \ DNSMADEEASY_API_SECRET=yyyyy \ -lego --dns dnsmadeeasy -d '*.example.com' -d example.com run +lego --email you@example.com --dns dnsmadeeasy --domains my.example.org run ''' [Configuration] @@ -16,10 +16,10 @@ lego --dns dnsmadeeasy -d '*.example.com' -d example.com run DNSMADEEASY_API_SECRET = "The API Secret key" [Configuration.Additional] DNSMADEEASY_SANDBOX = "Activate the sandbox (boolean)" - DNSMADEEASY_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - DNSMADEEASY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - DNSMADEEASY_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - DNSMADEEASY_HTTP_TIMEOUT = "API request timeout in seconds (Default: 10)" + DNSMADEEASY_POLLING_INTERVAL = "Time between DNS propagation check" + DNSMADEEASY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + DNSMADEEASY_TTL = "The TTL of the TXT record used for the DNS challenge" + DNSMADEEASY_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://api-docs.dnsmadeeasy.com/" diff --git a/providers/dns/dnsmadeeasy/dnsmadeeasy_test.go b/providers/dns/dnsmadeeasy/dnsmadeeasy_test.go index f6fc2e273..5c508e60d 100644 --- a/providers/dns/dnsmadeeasy/dnsmadeeasy_test.go +++ b/providers/dns/dnsmadeeasy/dnsmadeeasy_test.go @@ -59,7 +59,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -136,7 +135,6 @@ func TestLivePresentAndCleanup(t *testing.T) { os.Setenv(EnvSandbox, "true") envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/dnsmadeeasy/internal/client.go b/providers/dns/dnsmadeeasy/internal/client.go index 7963ad614..491d5fd98 100644 --- a/providers/dns/dnsmadeeasy/internal/client.go +++ b/providers/dns/dnsmadeeasy/internal/client.go @@ -15,7 +15,6 @@ import ( "strconv" "time" - "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/providers/dns/internal/errutils" ) @@ -58,8 +57,10 @@ func NewClient(apiKey, apiSecret string) (*Client, error) { func (c *Client) GetDomain(ctx context.Context, authZone string) (*Domain, error) { endpoint := c.BaseURL.JoinPath("dns", "managed", "name") + domainName := authZone[0 : len(authZone)-1] + query := endpoint.Query() - query.Set("domainname", dns01.UnFqdn(authZone)) + query.Set("domainname", domainName) endpoint.RawQuery = query.Encode() req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) @@ -68,7 +69,6 @@ func (c *Client) GetDomain(ctx context.Context, authZone string) (*Domain, error } domain := &Domain{} - err = c.do(req, domain) if err != nil { return nil, err @@ -92,7 +92,6 @@ func (c *Client) GetRecords(ctx context.Context, domain *Domain, recordName, rec } records := &recordsResponse{} - err = c.do(req, records) if err != nil { return nil, err @@ -174,12 +173,10 @@ func (c *Client) sign(req *http.Request, timestamp string) error { func computeHMAC(message, secret string) (string, error) { key := []byte(secret) h := hmac.New(sha1.New, key) - _, err := h.Write([]byte(message)) if err != nil { return "", err } - return hex.EncodeToString(h.Sum(nil)), nil } diff --git a/providers/dns/dnsmadeeasy/internal/client_test.go b/providers/dns/dnsmadeeasy/internal/client_test.go index cde212fc8..721214693 100644 --- a/providers/dns/dnsmadeeasy/internal/client_test.go +++ b/providers/dns/dnsmadeeasy/internal/client_test.go @@ -2,132 +2,14 @@ package internal import ( "net/http" - "net/http/httptest" - "net/url" "testing" "time" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("key", "secret") - if err != nil { - return nil, err - } - - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - With("x-dnsme-apiKey", "key"). - WithRegexp("x-dnsme-requestDate", `\w+, \d+ \w+ \d+ \d+:\d+:\d+ UTC`). - WithRegexp("x-dnsme-hmac", `[a-z0-9]+`), - ) -} - -func TestClient_GetDomain(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/managed/name", - servermock.RawStringResponse(`{"id": 1, "name": "foo"}`), - servermock.CheckQueryParameter().Strict(). - With("domainname", "example.com")). - Build(t) - - domain, err := client.GetDomain(t.Context(), "example.com.") - require.NoError(t, err) - - expected := &Domain{ - ID: 1, - Name: "foo", - } - - assert.Equal(t, expected, domain) -} - -func TestClient_GetRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/managed/1/records", - servermock.ResponseFromFixture("get_records.json"), - servermock.CheckQueryParameter().Strict(). - With("recordName", "foo"). - With("type", "TXT"), - ). - Build(t) - - domain := &Domain{ID: 1, Name: "foo"} - - records, err := client.GetRecords(t.Context(), domain, "foo", "TXT") - require.NoError(t, err) - - expected := []Record{ - { - ID: 1, - Type: "TXT", - Name: "foo", - Value: "aaa", - TTL: 60, - SourceID: 123, - }, - { - ID: 2, - Type: "TXT", - Name: "bar", - Value: "bbb", - TTL: 120, - SourceID: 456, - }, - } - - assert.Equal(t, &expected, records) -} - -func TestClient_CreateRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /dns/managed/1/records", nil, - servermock.CheckRequestJSONBodyFromFixture("create_record-request.json")). - Build(t) - - domain := &Domain{ID: 1, Name: "foo"} - - record := &Record{ - ID: 1, - Type: "TXT", - Name: "foo", - Value: "aaa", - TTL: 60, - SourceID: 123, - } - - err := client.CreateRecord(t.Context(), domain, record) - require.NoError(t, err) -} - -func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /dns/managed/123/records/1", nil). - Build(t) - - record := Record{ - ID: 1, - Type: "TXT", - Name: "foo", - Value: "aaa", - TTL: 60, - SourceID: 123, - } - - err := client.DeleteRecord(t.Context(), record) - require.NoError(t, err) -} - -func TestClient_sign(t *testing.T) { +func Test_sign(t *testing.T) { apiKey := "key" client := Client{apiKey: apiKey, apiSecret: "secret"} diff --git a/providers/dns/dnsmadeeasy/internal/fixtures/create_record-request.json b/providers/dns/dnsmadeeasy/internal/fixtures/create_record-request.json deleted file mode 100644 index 9a08b6544..000000000 --- a/providers/dns/dnsmadeeasy/internal/fixtures/create_record-request.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": 1, - "type": "TXT", - "name": "foo", - "value": "aaa", - "ttl": 60, - "sourceId": 123 -} diff --git a/providers/dns/dnsmadeeasy/internal/fixtures/get_records.json b/providers/dns/dnsmadeeasy/internal/fixtures/get_records.json deleted file mode 100644 index 5667e5e1d..000000000 --- a/providers/dns/dnsmadeeasy/internal/fixtures/get_records.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "data": [ - { - "id": 1, - "type": "TXT", - "name": "foo", - "value": "aaa", - "ttl": 60, - "sourceId": 123 - }, - { - "id": 2, - "type": "TXT", - "name": "bar", - "value": "bbb", - "ttl": 120, - "sourceId": 456 - } - ] -} diff --git a/providers/dns/dnspod/dnspod.go b/providers/dns/dnspod/dnspod.go index 52a873c7b..5f8e84880 100644 --- a/providers/dns/dnspod/dnspod.go +++ b/providers/dns/dnspod/dnspod.go @@ -8,10 +8,8 @@ import ( "strconv" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/nrdcg/dnspod-go" ) @@ -27,8 +25,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { LoginToken string @@ -83,12 +79,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { params := dnspod.CommonParams{LoginToken: config.LoginToken, Format: "json"} client := dnspod.NewClient(params) - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + client.HTTPClient = config.HTTPClient return &DNSProvider{client: client, config: config}, nil } @@ -135,7 +126,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return err } } - return nil } @@ -157,7 +147,6 @@ func (d *DNSProvider) getHostedZone(domain string) (string, string, error) { } var hostedZone dnspod.Domain - for _, zone := range zones { if zone.Name == dns01.UnFqdn(authZone) { hostedZone = zone @@ -165,7 +154,7 @@ func (d *DNSProvider) getHostedZone(domain string) (string, string, error) { } if hostedZone.ID == "" || hostedZone.ID == "0" { - return "", "", fmt.Errorf("zone %s not found for domain %s", authZone, domain) + return "", "", fmt.Errorf("zone %s not found in dnspod for domain %s", authZone, domain) } return hostedZone.ID.String(), hostedZone.Name, nil @@ -193,7 +182,6 @@ func (d *DNSProvider) findTxtRecords(fqdn, zoneID, zoneName string) ([]dnspod.Re } var records []dnspod.Record - result, _, err := d.client.Records.List(zoneID, subDomain) if err != nil { return records, fmt.Errorf("API call has failed: %w", err) diff --git a/providers/dns/dnspod/dnspod.toml b/providers/dns/dnspod/dnspod.toml index 162685d76..ff1535595 100644 --- a/providers/dns/dnspod/dnspod.toml +++ b/providers/dns/dnspod/dnspod.toml @@ -8,17 +8,17 @@ Since = "v0.4.0" Example = ''' DNSPOD_API_KEY=xxxxxx \ -lego --dns dnspod -d '*.example.com' -d example.com run +lego --email you@example.com --dns dnspod --domains my.example.org run ''' [Configuration] [Configuration.Credentials] DNSPOD_API_KEY = "The user token" [Configuration.Additional] - DNSPOD_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - DNSPOD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - DNSPOD_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)" - DNSPOD_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + DNSPOD_POLLING_INTERVAL = "Time between DNS propagation check" + DNSPOD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + DNSPOD_TTL = "The TTL of the TXT record used for the DNS challenge" + DNSPOD_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://docs.dnspod.com/api/" diff --git a/providers/dns/dnspod/dnspod_test.go b/providers/dns/dnspod/dnspod_test.go index 5d339353a..640ec34c6 100644 --- a/providers/dns/dnspod/dnspod_test.go +++ b/providers/dns/dnspod/dnspod_test.go @@ -37,7 +37,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -97,7 +96,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -111,7 +109,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/dode/dode.go b/providers/dns/dode/dode.go index 59ad785e8..04393fb05 100644 --- a/providers/dns/dode/dode.go +++ b/providers/dns/dode/dode.go @@ -8,11 +8,9 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/dode/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -27,8 +25,6 @@ const ( EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Token string @@ -86,8 +82,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{config: config, client: client}, nil } diff --git a/providers/dns/dode/dode.toml b/providers/dns/dode/dode.toml index eb629bb3e..c352d249a 100644 --- a/providers/dns/dode/dode.toml +++ b/providers/dns/dode/dode.toml @@ -6,17 +6,18 @@ Since = "v2.4.0" Example = ''' DODE_TOKEN=xxxxxx \ -lego --dns dode -d '*.example.com' -d example.com run +lego --email you@example.com --dns dode --domains my.example.org run ''' [Configuration] [Configuration.Credentials] DODE_TOKEN = "API token" [Configuration.Additional] - DODE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - DODE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - DODE_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - DODE_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 60)" + DODE_POLLING_INTERVAL = "Time between DNS propagation check" + DODE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + DODE_TTL = "The TTL of the TXT record used for the DNS challenge" + DODE_HTTP_TIMEOUT = "API request timeout" + DODE_SEQUENCE_INTERVAL = "Time between sequential requests" [Links] API = "https://www.do.de/wiki/freie-ssl-tls-zertifikate-ueber-acme/" diff --git a/providers/dns/dode/dode_test.go b/providers/dns/dode/dode_test.go index fefcc79b1..3d8e9395a 100644 --- a/providers/dns/dode/dode_test.go +++ b/providers/dns/dode/dode_test.go @@ -36,7 +36,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -94,7 +93,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -108,7 +106,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/dode/internal/client.go b/providers/dns/dode/internal/client.go index 6824e7c48..39085ebdb 100644 --- a/providers/dns/dode/internal/client.go +++ b/providers/dns/dode/internal/client.go @@ -36,7 +36,7 @@ func NewClient(token string) *Client { // UpdateTxtRecord Update the domains TXT record // To update the TXT record we just need to make one simple get request. -func (c *Client) UpdateTxtRecord(ctx context.Context, fqdn, txt string, clearRecord bool) error { +func (c Client) UpdateTxtRecord(ctx context.Context, fqdn, txt string, clear bool) error { endpoint := c.baseURL.JoinPath("letsencrypt") query := endpoint.Query() @@ -44,7 +44,7 @@ func (c *Client) UpdateTxtRecord(ctx context.Context, fqdn, txt string, clearRec query.Set("domain", dns01.UnFqdn(fqdn)) // api call differs per set/delete - if clearRecord { + if clear { query.Set("action", "delete") } else { query.Set("value", txt) @@ -70,7 +70,6 @@ func (c *Client) UpdateTxtRecord(ctx context.Context, fqdn, txt string, clearRec } var response apiResponse - err = json.Unmarshal(raw, &response) if err != nil { return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) diff --git a/providers/dns/dode/internal/client_test.go b/providers/dns/dode/internal/client_test.go index 6fbaa8c1d..116ca8c4c 100644 --- a/providers/dns/dode/internal/client_test.go +++ b/providers/dns/dode/internal/client_test.go @@ -1,44 +1,93 @@ package internal import ( + "context" + "fmt" + "io" + "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/require" ) -func setupClient(server *httptest.Server) (*Client, error) { +func setupTest(t *testing.T, method, pattern string, status int, file string) *Client { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest) + return + } + + query := req.URL.Query() + if query.Get("token") != "secret" { + http.Error(rw, fmt.Sprintf("invalid credentials: %q", query.Get("token")), http.StatusUnauthorized) + return + } + + if query.Get("domain") != "example.com" { + http.Error(rw, fmt.Sprintf("invalid domain: %q", query.Get("domain")), http.StatusBadRequest) + return + } + + if query.Has("action") { + if query.Get("action") != "delete" { + http.Error(rw, fmt.Sprintf("invalid action: %q", query.Get("action")), http.StatusBadRequest) + return + } + } else { + if query.Get("value") != "value" { + http.Error(rw, fmt.Sprintf("invalid value: %q", query.Get("value")), http.StatusBadRequest) + return + } + } + + if file == "" { + rw.WriteHeader(status) + return + } + + open, err := os.Open(filepath.Join("fixtures", file)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = open.Close() }() + + rw.WriteHeader(status) + _, err = io.Copy(rw, open) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + client := NewClient("secret") client.HTTPClient = server.Client() client.baseURL, _ = url.Parse(server.URL) - return client, nil + return client } func TestClient_UpdateTxtRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /letsencrypt", servermock.ResponseFromFixture("success.json"), - servermock.CheckQueryParameter().Strict(). - With("domain", "example.com"). - With("token", "secret"). - With("value", "value")). - Build(t) + client := setupTest(t, http.MethodGet, "/letsencrypt", http.StatusOK, "success.json") - err := client.UpdateTxtRecord(t.Context(), "example.com.", "value", false) + err := client.UpdateTxtRecord(context.Background(), "example.com.", "value", false) require.NoError(t, err) } func TestClient_UpdateTxtRecord_clear(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /letsencrypt", servermock.ResponseFromFixture("success.json"), - servermock.CheckQueryParameter().Strict(). - With("action", "delete"). - With("domain", "example.com"). - With("token", "secret")). - Build(t) + client := setupTest(t, http.MethodGet, "/letsencrypt", http.StatusOK, "success.json") - err := client.UpdateTxtRecord(t.Context(), "example.com.", "value", true) + err := client.UpdateTxtRecord(context.Background(), "example.com.", "value", true) require.NoError(t, err) } diff --git a/providers/dns/domeneshop/domeneshop.go b/providers/dns/domeneshop/domeneshop.go index fb16b442e..d074ba53f 100644 --- a/providers/dns/domeneshop/domeneshop.go +++ b/providers/dns/domeneshop/domeneshop.go @@ -8,11 +8,9 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/domeneshop/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -27,8 +25,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIToken string @@ -87,8 +83,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{config: config, client: client}, nil } diff --git a/providers/dns/domeneshop/domeneshop.toml b/providers/dns/domeneshop/domeneshop.toml index b74af598e..5dffea7b6 100644 --- a/providers/dns/domeneshop/domeneshop.toml +++ b/providers/dns/domeneshop/domeneshop.toml @@ -2,13 +2,12 @@ Name = "Domeneshop" Description = '''''' URL = "https://domene.shop" Code = "domeneshop" -Aliases = ["domainnameshop"] Since = "v4.3.0" Example = ''' DOMENESHOP_API_TOKEN= \ DOMENESHOP_API_SECRET= \ -lego --dns domeneshop -d '*.example.com' -d example.com run +lego --email example@example.com --dns domeneshop --domains example.com run ''' Additional = ''' @@ -24,9 +23,9 @@ Visit the following page for information on how to create API credentials with D DOMENESHOP_API_TOKEN = "API token" DOMENESHOP_API_SECRET = "API secret" [Configuration.Additional] - DOMENESHOP_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 20)" - DOMENESHOP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 300)" - DOMENESHOP_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + DOMENESHOP_POLLING_INTERVAL = "Time between DNS propagation check" + DOMENESHOP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + DOMENESHOP_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://api.domeneshop.no/docs" diff --git a/providers/dns/domeneshop/domeneshop_test.go b/providers/dns/domeneshop/domeneshop_test.go index 086efd44a..389975bca 100644 --- a/providers/dns/domeneshop/domeneshop_test.go +++ b/providers/dns/domeneshop/domeneshop_test.go @@ -57,7 +57,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -131,7 +130,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -145,7 +143,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/domeneshop/internal/client.go b/providers/dns/domeneshop/internal/client.go index 9ab964222..b7ebb9940 100644 --- a/providers/dns/domeneshop/internal/client.go +++ b/providers/dns/domeneshop/internal/client.go @@ -72,7 +72,7 @@ func (c *Client) GetDomainByName(ctx context.Context, domain string) (*Domain, e // CreateTXTRecord creates a TXT record with the provided host (subdomain) and data. // https://api.domeneshop.no/docs/#tag/dns/paths/~1domains~1{domainId}~1dns/post -func (c *Client) CreateTXTRecord(ctx context.Context, domain *Domain, host, data string) error { +func (c *Client) CreateTXTRecord(ctx context.Context, domain *Domain, host string, data string) error { endpoint := c.baseURL.JoinPath("domains", strconv.Itoa(domain.ID), "dns") record := DNSRecord{ @@ -92,7 +92,7 @@ func (c *Client) CreateTXTRecord(ctx context.Context, domain *Domain, host, data // DeleteTXTRecord deletes the DNS record matching the provided host and data. // https://api.domeneshop.no/docs/#tag/dns/paths/~1domains~1{domainId}~1dns~1{recordId}/delete -func (c *Client) DeleteTXTRecord(ctx context.Context, domain *Domain, host, data string) error { +func (c *Client) DeleteTXTRecord(ctx context.Context, domain *Domain, host string, data string) error { record, err := c.getDNSRecordByHostData(ctx, *domain, host, data) if err != nil { return err @@ -110,7 +110,7 @@ func (c *Client) DeleteTXTRecord(ctx context.Context, domain *Domain, host, data // getDNSRecordByHostData finds the first matching DNS record with the provided host and data. // https://api.domeneshop.no/docs/#operation/getDnsRecords -func (c *Client) getDNSRecordByHostData(ctx context.Context, domain Domain, host, data string) (*DNSRecord, error) { +func (c *Client) getDNSRecordByHostData(ctx context.Context, domain Domain, host string, data string) (*DNSRecord, error) { endpoint := c.baseURL.JoinPath("domains", strconv.Itoa(domain.ID), "dns") req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) diff --git a/providers/dns/domeneshop/internal/client_test.go b/providers/dns/domeneshop/internal/client_test.go index 2f5fb0d95..71205cac4 100644 --- a/providers/dns/domeneshop/internal/client_test.go +++ b/providers/dns/domeneshop/internal/client_test.go @@ -1,58 +1,124 @@ package internal import ( + "context" + "net/http" "net/http/httptest" "net/url" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("token", "secret") - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) +const authorizationHeader = "Authorization" - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithBasicAuth("token", "secret"), - ) +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient("token", "secret") + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client, mux } func TestClient_CreateTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/1/dns", - servermock.ResponseFromFixture("create_record.json"), - servermock.CheckRequestJSONBodyFromFixture("create_record-request.json")). - Build(t) + client, mux := setupTest(t) - err := client.CreateTXTRecord(t.Context(), &Domain{ID: 1}, "example.com", "txtTXTtxt") + mux.HandleFunc("/domains/1/dns", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + auth := req.Header.Get(authorizationHeader) + if auth != "Basic dG9rZW46c2VjcmV0" { + http.Error(rw, "invalid credentials: "+auth, http.StatusUnauthorized) + return + } + + _, _ = rw.Write([]byte(`{"id": 1}`)) + }) + + err := client.CreateTXTRecord(context.Background(), &Domain{ID: 1}, "example", "txtTXTtxt") require.NoError(t, err) } func TestClient_DeleteTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("GET /domains/1/dns", - servermock.ResponseFromFixture("delete_record.json")). - Route("DELETE /domains/1/dns/1", nil). - Build(t) + client, mux := setupTest(t) - err := client.DeleteTXTRecord(t.Context(), &Domain{ID: 1}, "example.com", "txtTXTtxt") + mux.HandleFunc("/domains/1/dns", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + auth := req.Header.Get(authorizationHeader) + if auth != "Basic dG9rZW46c2VjcmV0" { + http.Error(rw, "invalid credentials: "+auth, http.StatusUnauthorized) + return + } + + _, _ = rw.Write([]byte(`[ + { + "id": 1, + "host": "example.com", + "ttl": 3600, + "type": "TXT", + "data": "txtTXTtxt" + } +]`)) + }) + + mux.HandleFunc("/domains/1/dns/1", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodDelete { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + auth := req.Header.Get(authorizationHeader) + if auth != "Basic dG9rZW46c2VjcmV0" { + http.Error(rw, "invalid credentials: "+auth, http.StatusUnauthorized) + return + } + }) + + err := client.DeleteTXTRecord(context.Background(), &Domain{ID: 1}, "example.com", "txtTXTtxt") require.NoError(t, err) } func TestClient_getDNSRecordByHostData(t *testing.T) { - client := mockBuilder(). - Route("GET /domains/1/dns", - servermock.ResponseFromFixture("getDnsRecords.json")). - Build(t) + client, mux := setupTest(t) - record, err := client.getDNSRecordByHostData(t.Context(), Domain{ID: 1}, "example.com", "txtTXTtxt") + mux.HandleFunc("/domains/1/dns", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + auth := req.Header.Get(authorizationHeader) + if auth != "Basic dG9rZW46c2VjcmV0" { + http.Error(rw, "invalid credentials: "+auth, http.StatusUnauthorized) + return + } + + _, _ = rw.Write([]byte(`[ + { + "id": 1, + "host": "example.com", + "ttl": 3600, + "type": "TXT", + "data": "txtTXTtxt" + } +]`)) + }) + + record, err := client.getDNSRecordByHostData(context.Background(), Domain{ID: 1}, "example.com", "txtTXTtxt") require.NoError(t, err) expected := &DNSRecord{ @@ -67,12 +133,45 @@ func TestClient_getDNSRecordByHostData(t *testing.T) { } func TestClient_GetDomainByName(t *testing.T) { - client := mockBuilder(). - Route("GET /domains/", - servermock.ResponseFromFixture("getDomains.json")). - Build(t) + client, mux := setupTest(t) - domain, err := client.GetDomainByName(t.Context(), "example.com") + mux.HandleFunc("/domains", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + auth := req.Header.Get(authorizationHeader) + if auth != "Basic dG9rZW46c2VjcmV0" { + http.Error(rw, "invalid credentials: "+auth, http.StatusUnauthorized) + return + } + + _, _ = rw.Write([]byte(`[ + { + "id": 1, + "domain": "example.com", + "expiry_date": "2019-08-24", + "registered_date": "2019-08-24", + "renew": true, + "registrant": "Ola Nordmann", + "status": "active", + "nameservers": [ + "ns1.hyp.net", + "ns2.hyp.net", + "ns3.hyp.net" + ], + "services": { + "registrar": true, + "dns": true, + "email": true, + "webhotel": "none" + } + } +]`)) + }) + + domain, err := client.GetDomainByName(context.Background(), "example.com") require.NoError(t, err) expected := &Domain{ diff --git a/providers/dns/domeneshop/internal/fixtures/create_record-request.json b/providers/dns/domeneshop/internal/fixtures/create_record-request.json deleted file mode 100644 index 6bd3ca4ce..000000000 --- a/providers/dns/domeneshop/internal/fixtures/create_record-request.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "data": "txtTXTtxt", - "host": "example.com", - "id": 0, - "ttl": 300, - "type": "TXT" -} diff --git a/providers/dns/domeneshop/internal/fixtures/create_record.json b/providers/dns/domeneshop/internal/fixtures/create_record.json deleted file mode 100644 index 2572ae5fe..000000000 --- a/providers/dns/domeneshop/internal/fixtures/create_record.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "id": 1 -} diff --git a/providers/dns/domeneshop/internal/fixtures/delete_record.json b/providers/dns/domeneshop/internal/fixtures/delete_record.json deleted file mode 100644 index f3f987eef..000000000 --- a/providers/dns/domeneshop/internal/fixtures/delete_record.json +++ /dev/null @@ -1,9 +0,0 @@ -[ - { - "id": 1, - "host": "example.com", - "ttl": 3600, - "type": "TXT", - "data": "txtTXTtxt" - } -] diff --git a/providers/dns/domeneshop/internal/fixtures/getDnsRecords.json b/providers/dns/domeneshop/internal/fixtures/getDnsRecords.json deleted file mode 100644 index f3f987eef..000000000 --- a/providers/dns/domeneshop/internal/fixtures/getDnsRecords.json +++ /dev/null @@ -1,9 +0,0 @@ -[ - { - "id": 1, - "host": "example.com", - "ttl": 3600, - "type": "TXT", - "data": "txtTXTtxt" - } -] diff --git a/providers/dns/domeneshop/internal/fixtures/getDomains.json b/providers/dns/domeneshop/internal/fixtures/getDomains.json deleted file mode 100644 index b491d7f53..000000000 --- a/providers/dns/domeneshop/internal/fixtures/getDomains.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "id": 1, - "domain": "example.com", - "expiry_date": "2019-08-24", - "registered_date": "2019-08-24", - "renew": true, - "registrant": "Ola Nordmann", - "status": "active", - "nameservers": [ - "ns1.hyp.net", - "ns2.hyp.net", - "ns3.hyp.net" - ], - "services": { - "registrar": true, - "dns": true, - "email": true, - "webhotel": "none" - } - } -] diff --git a/providers/dns/dreamhost/dreamhost.go b/providers/dns/dreamhost/dreamhost.go index 8663e18ce..8f0c850df 100644 --- a/providers/dns/dreamhost/dreamhost.go +++ b/providers/dns/dreamhost/dreamhost.go @@ -10,11 +10,9 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/dreamhost/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -28,8 +26,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string @@ -87,8 +83,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - if config.BaseURL != "" { client.BaseURL = config.BaseURL } @@ -99,7 +93,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) - err := d.client.AddRecord(context.Background(), dns01.UnFqdn(info.EffectiveFQDN), info.Value) if err != nil { return fmt.Errorf("dreamhost: %w", err) diff --git a/providers/dns/dreamhost/dreamhost.toml b/providers/dns/dreamhost/dreamhost.toml index c3a9db360..176848d4d 100644 --- a/providers/dns/dreamhost/dreamhost.toml +++ b/providers/dns/dreamhost/dreamhost.toml @@ -6,16 +6,17 @@ Since = "v1.1.0" Example = ''' DREAMHOST_API_KEY="YOURAPIKEY" \ -lego --dns dreamhost -d '*.example.com' -d example.com run +lego --email you@example.com --dns dreamhost --domains my.example.org run ''' [Configuration] [Configuration.Credentials] DREAMHOST_API_KEY = "The API key" [Configuration.Additional] - DREAMHOST_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 60)" - DREAMHOST_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 3600)" - DREAMHOST_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + DREAMHOST_POLLING_INTERVAL = "Time between DNS propagation check" + DREAMHOST_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + DREAMHOST_TTL = "The TTL of the TXT record used for the DNS challenge" + DREAMHOST_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://help.dreamhost.com/hc/en-us/articles/217560167-API_overview" diff --git a/providers/dns/dreamhost/dreamhost_test.go b/providers/dns/dreamhost/dreamhost_test.go index 5af0b892d..0f91ffae2 100644 --- a/providers/dns/dreamhost/dreamhost_test.go +++ b/providers/dns/dreamhost/dreamhost_test.go @@ -1,12 +1,13 @@ package dreamhost import ( + "fmt" + "net/http" "net/http/httptest" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -22,15 +23,22 @@ const ( fakeKeyAuth = "w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI" ) -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIKey = fakeAPIKey - config.BaseURL = server.URL - config.HTTPClient = server.Client() +func setupTest(t *testing.T) (*DNSProvider, *http.ServeMux) { + t.Helper() - return NewDNSProviderConfig(config) - }) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + config := NewDefaultConfig() + config.APIKey = fakeAPIKey + config.BaseURL = server.URL + config.HTTPClient = server.Client() + + provider, err := NewDNSProviderConfig(config) + require.NoError(t, err) + + return provider, mux } func TestNewDNSProvider(t *testing.T) { @@ -57,7 +65,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -108,51 +115,70 @@ func TestNewDNSProviderConfig(t *testing.T) { } func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("GET /", - servermock.RawStringResponse(`{"data":"record_added","result":"success"}`), - servermock.CheckQueryParameter().Strict(). - With("cmd", "dns-add_record"). - With("comment", "Managed+By+lego"). - With("format", "json"). - With("record", "_acme-challenge.example.com"). - With("type", "TXT"). - With("key", fakeAPIKey). - With("value", fakeKeyAuth), - ). - Build(t) + provider, mux := setupTest(t) + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method, "method") + + q := r.URL.Query() + assert.Equal(t, fakeAPIKey, q.Get("key")) + assert.Equal(t, "dns-add_record", q.Get("cmd")) + assert.Equal(t, "json", q.Get("format")) + assert.Equal(t, "_acme-challenge.example.com", q.Get("record")) + assert.Equal(t, fakeKeyAuth, q.Get("value")) + assert.Equal(t, "Managed+By+lego", q.Get("comment")) + + _, err := fmt.Fprintf(w, `{"data":"record_added","result":"success"}`) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) err := provider.Present("example.com", "", fakeChallengeToken) require.NoError(t, err) } func TestDNSProvider_PresentFailed(t *testing.T) { - provider := mockBuilder(). - Route("GET /", - servermock.RawStringResponse(`{"data":"record_already_exists_remove_first","result":"error"}`)). - Build(t) + provider, mux := setupTest(t) + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method, "method") + + _, err := fmt.Fprintf(w, `{"data":"record_already_exists_remove_first","result":"error"}`) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) err := provider.Present("example.com", "", fakeChallengeToken) require.EqualError(t, err, "dreamhost: add TXT record failed: record_already_exists_remove_first") } func TestDNSProvider_Cleanup(t *testing.T) { - provider := mockBuilder(). - Route("GET /", - servermock.RawStringResponse(`{"data":"record_removed","result":"success"}`), - servermock.CheckQueryParameter().Strict(). - With("cmd", "dns-remove_record"). - With("comment", "Managed+By+lego"). - With("format", "json"). - With("record", "_acme-challenge.example.com"). - With("type", "TXT"). - With("key", fakeAPIKey). - With("value", fakeKeyAuth), - ). - Build(t) + provider, mux := setupTest(t) + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method, "method") + + q := r.URL.Query() + assert.Equal(t, fakeAPIKey, q.Get("key"), "key mismatch") + assert.Equal(t, "dns-remove_record", q.Get("cmd"), "cmd mismatch") + assert.Equal(t, "json", q.Get("format")) + assert.Equal(t, "_acme-challenge.example.com", q.Get("record")) + assert.Equal(t, fakeKeyAuth, q.Get("value"), "value mismatch") + assert.Equal(t, "Managed+By+lego", q.Get("comment")) + + _, err := fmt.Fprintf(w, `{"data":"record_removed","result":"success"}`) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) err := provider.CleanUp("example.com", "", fakeChallengeToken) - require.NoError(t, err) + require.NoError(t, err, "failed to remove TXT record") } func TestLivePresentAndCleanUp(t *testing.T) { @@ -161,7 +187,6 @@ func TestLivePresentAndCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/dreamhost/internal/client.go b/providers/dns/dreamhost/internal/client.go index 02b33ad57..dee808ac8 100644 --- a/providers/dns/dreamhost/internal/client.go +++ b/providers/dns/dreamhost/internal/client.go @@ -101,7 +101,6 @@ func (c *Client) updateTxtRecord(ctx context.Context, endpoint *url.URL) error { } var response apiResponse - err = json.Unmarshal(raw, &response) if err != nil { return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) diff --git a/providers/dns/dreamhost/internal/client_test.go b/providers/dns/dreamhost/internal/client_test.go index a836658f9..eff520df0 100644 --- a/providers/dns/dreamhost/internal/client_test.go +++ b/providers/dns/dreamhost/internal/client_test.go @@ -1,59 +1,15 @@ package internal import ( - "net/http/httptest" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func setupClient(server *httptest.Server) (*Client, error) { - client := NewClient("secret") - client.BaseURL = server.URL - client.HTTPClient = server.Client() - - return client, nil -} - -func TestClient_AddRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /", servermock.RawStringResponse(`{}`), - servermock.CheckQueryParameter().Strict(). - With("cmd", "dns-add_record"). - With("comment", "Managed+By+lego"). - With("format", "json"). - With("key", "secret"). - With("record", "example.com"). - With("type", "TXT"). - With("value", "aaa")). - Build(t) - - err := client.AddRecord(t.Context(), "example.com", "aaa") - require.NoError(t, err) -} - -func TestClient_RemoveRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /", servermock.RawStringResponse(`{}`), - servermock.CheckQueryParameter().Strict(). - With("cmd", "dns-remove_record"). - With("comment", "Managed+By+lego"). - With("format", "json"). - With("key", "secret"). - With("record", "example.com"). - With("type", "TXT"). - With("value", "aaa")). - Build(t) - - err := client.RemoveRecord(t.Context(), "example.com", "aaa") - require.NoError(t, err) -} +const fakeAPIKey = "asdf1234" func TestClient_buildQuery(t *testing.T) { - const fakeAPIKey = "asdf1234" - testCases := []struct { desc string apiKey string diff --git a/providers/dns/duckdns/duckdns.go b/providers/dns/duckdns/duckdns.go index 1aae0a06c..8cb82aed4 100644 --- a/providers/dns/duckdns/duckdns.go +++ b/providers/dns/duckdns/duckdns.go @@ -9,11 +9,9 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/duckdns/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -28,8 +26,6 @@ const ( EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Token string @@ -87,8 +83,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{config: config, client: client}, nil } diff --git a/providers/dns/duckdns/duckdns.toml b/providers/dns/duckdns/duckdns.toml index 6866da57c..ae6b318b9 100644 --- a/providers/dns/duckdns/duckdns.toml +++ b/providers/dns/duckdns/duckdns.toml @@ -6,17 +6,18 @@ Since = "v0.5.0" Example = ''' DUCKDNS_TOKEN=xxxxxx \ -lego --dns duckdns -d '*.example.com' -d example.com run +lego --email you@example.com --dns duckdns --domains my.example.org run ''' [Configuration] [Configuration.Credentials] DUCKDNS_TOKEN = "Account token" [Configuration.Additional] - DUCKDNS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - DUCKDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - DUCKDNS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - DUCKDNS_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 60)" + DUCKDNS_POLLING_INTERVAL = "Time between DNS propagation check" + DUCKDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + DUCKDNS_TTL = "The TTL of the TXT record used for the DNS challenge" + DUCKDNS_HTTP_TIMEOUT = "API request timeout" + DUCKDNS_SEQUENCE_INTERVAL = "Time between sequential requests" [Links] API = "https://www.duckdns.org/spec.jsp" diff --git a/providers/dns/duckdns/duckdns_test.go b/providers/dns/duckdns/duckdns_test.go index 769513fbf..b89966a36 100644 --- a/providers/dns/duckdns/duckdns_test.go +++ b/providers/dns/duckdns/duckdns_test.go @@ -37,7 +37,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -95,7 +94,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -109,7 +107,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/duckdns/internal/client.go b/providers/dns/duckdns/internal/client.go index c5d7ef97c..e1985ee7a 100644 --- a/providers/dns/duckdns/internal/client.go +++ b/providers/dns/duckdns/internal/client.go @@ -21,7 +21,6 @@ const defaultBaseURL = "https://www.duckdns.org/update" type Client struct { token string - baseURL string HTTPClient *http.Client } @@ -29,24 +28,23 @@ type Client struct { func NewClient(token string) *Client { return &Client{ token: token, - baseURL: defaultBaseURL, HTTPClient: &http.Client{Timeout: 5 * time.Second}, } } -func (c *Client) AddTXTRecord(ctx context.Context, domain, value string) error { +func (c Client) AddTXTRecord(ctx context.Context, domain, value string) error { return c.UpdateTxtRecord(ctx, domain, value, false) } -func (c *Client) RemoveTXTRecord(ctx context.Context, domain string) error { +func (c Client) RemoveTXTRecord(ctx context.Context, domain string) error { return c.UpdateTxtRecord(ctx, domain, "", true) } // UpdateTxtRecord Update the domains TXT record // To update the TXT record we just need to make one simple get request. // In DuckDNS you only have one TXT record shared with the domain and all subdomains. -func (c *Client) UpdateTxtRecord(ctx context.Context, domain, txt string, clearRecord bool) error { - endpoint, _ := url.Parse(c.baseURL) +func (c Client) UpdateTxtRecord(ctx context.Context, domain, txt string, clear bool) error { + endpoint, _ := url.Parse(defaultBaseURL) mainDomain := getMainDomain(domain) if mainDomain == "" { @@ -56,7 +54,7 @@ func (c *Client) UpdateTxtRecord(ctx context.Context, domain, txt string, clearR query := endpoint.Query() query.Set("domains", mainDomain) query.Set("token", c.token) - query.Set("clear", strconv.FormatBool(clearRecord)) + query.Set("clear", strconv.FormatBool(clear)) query.Set("txt", txt) endpoint.RawQuery = query.Encode() @@ -81,7 +79,6 @@ func (c *Client) UpdateTxtRecord(ctx context.Context, domain, txt string, clearR if body != "OK" { return fmt.Errorf("request to change TXT record for DuckDNS returned the following result (%s) this does not match expectation (OK) used url [%s]", body, endpoint) } - return nil } @@ -99,7 +96,6 @@ func getMainDomain(domain string) string { } firstSubDomainIndex := split[len(split)-3] - return domain[firstSubDomainIndex:] } diff --git a/providers/dns/duckdns/internal/client_test.go b/providers/dns/duckdns/internal/client_test.go index aaa441fad..4df17d049 100644 --- a/providers/dns/duckdns/internal/client_test.go +++ b/providers/dns/duckdns/internal/client_test.go @@ -1,50 +1,11 @@ package internal import ( - "net/http/httptest" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) -func setupClient(server *httptest.Server) (*Client, error) { - client := NewClient("secret") - client.baseURL = server.URL - client.HTTPClient = server.Client() - - return client, nil -} - -func TestClient_AddTXTRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /", servermock.RawStringResponse("OK"), - servermock.CheckQueryParameter().Strict(). - With("clear", "false"). - With("domains", "com"). - With("token", "secret"). - With("txt", "value")). - Build(t) - - err := client.AddTXTRecord(t.Context(), "example.com", "value") - require.NoError(t, err) -} - -func TestClient_RemoveTXTRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /", servermock.RawStringResponse("OK"), - servermock.CheckQueryParameter().Strict(). - With("clear", "true"). - With("domains", "com"). - With("token", "secret"). - With("txt", "")). - Build(t) - - err := client.RemoveTXTRecord(t.Context(), "example.com") - require.NoError(t, err) -} - func Test_getMainDomain(t *testing.T) { testCases := []struct { desc string diff --git a/providers/dns/dyn/dyn.go b/providers/dns/dyn/dyn.go index 0cd445c39..3435110e5 100644 --- a/providers/dns/dyn/dyn.go +++ b/providers/dns/dyn/dyn.go @@ -8,11 +8,9 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/dyn/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -29,8 +27,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { CustomerName string @@ -93,8 +89,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{config: config, client: client}, nil } diff --git a/providers/dns/dyn/dyn.toml b/providers/dns/dyn/dyn.toml index c4b3563e0..dc754fe01 100644 --- a/providers/dns/dyn/dyn.toml +++ b/providers/dns/dyn/dyn.toml @@ -8,7 +8,7 @@ Example = ''' DYN_CUSTOMER_NAME=xxxxxx \ DYN_USER_NAME=yyyyy \ DYN_PASSWORD=zzzz \ -lego --dns dyn -d '*.example.com' -d example.com run +lego --email you@example.com --dns dyn --domains my.example.org run ''' [Configuration] @@ -17,10 +17,10 @@ lego --dns dyn -d '*.example.com' -d example.com run DYN_USER_NAME = "User name" DYN_PASSWORD = "Password" [Configuration.Additional] - DYN_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - DYN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - DYN_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - DYN_HTTP_TIMEOUT = "API request timeout in seconds (Default: 10)" + DYN_POLLING_INTERVAL = "Time between DNS propagation check" + DYN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + DYN_TTL = "The TTL of the TXT record used for the DNS challenge" + DYN_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://help.dyn.com/rest/" diff --git a/providers/dns/dyn/dyn_test.go b/providers/dns/dyn/dyn_test.go index 5b4d1c6b6..25f1f5614 100644 --- a/providers/dns/dyn/dyn_test.go +++ b/providers/dns/dyn/dyn_test.go @@ -71,7 +71,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -156,7 +155,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -170,7 +168,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/dyn/internal/client.go b/providers/dns/dyn/internal/client.go index a54113eec..43981cc44 100644 --- a/providers/dns/dyn/internal/client.go +++ b/providers/dns/dyn/internal/client.go @@ -28,7 +28,7 @@ type Client struct { } // NewClient Creates a new Client. -func NewClient(customerName, username, password string) *Client { +func NewClient(customerName string, username string, password string) *Client { baseURL, _ := url.Parse(defaultBaseURL) return &Client{ @@ -127,7 +127,6 @@ func (c *Client) do(req *http.Request) (*APIResponse, error) { } var response APIResponse - err = json.Unmarshal(raw, &response) if err != nil { return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) diff --git a/providers/dns/dyn/internal/client_test.go b/providers/dns/dyn/internal/client_test.go index f166e7d8d..87bee1cd3 100644 --- a/providers/dns/dyn/internal/client_test.go +++ b/providers/dns/dyn/internal/client_test.go @@ -1,59 +1,122 @@ package internal import ( + "context" + "fmt" + "io" + "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/require" ) -func setupClient(server *httptest.Server) (*Client, error) { +func setupTest(t *testing.T, pattern string, handlerFunc http.HandlerFunc) *Client { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc(pattern, handlerFunc) + client := NewClient("bob", "user", "secret") client.HTTPClient = server.Client() client.baseURL, _ = url.Parse(server.URL) - return client, nil + return client } -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("bob", "user", "secret") - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) +func authenticatedHandler(method string, status int, file string) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest) + return + } - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders()) + token := req.Header.Get(authTokenHeader) + if token != "tok" { + http.Error(rw, fmt.Sprintf("invalid credentials: %q", token), http.StatusUnauthorized) + return + } + + if file == "" { + rw.WriteHeader(status) + return + } + + open, err := os.Open(filepath.Join("fixtures", file)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = open.Close() }() + + rw.WriteHeader(status) + _, err = io.Copy(rw, open) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + } +} + +func unauthenticatedHandler(method string, status int, file string) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest) + return + } + + token := req.Header.Get(authTokenHeader) + if token != "" { + http.Error(rw, fmt.Sprintf("invalid credentials: %q", token), http.StatusUnauthorized) + return + } + + if file == "" { + rw.WriteHeader(status) + return + } + + open, err := os.Open(filepath.Join("fixtures", file)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = open.Close() }() + + rw.WriteHeader(status) + _, err = io.Copy(rw, open) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + } } func TestClient_Publish(t *testing.T) { - client := mockBuilder(). - Route("PUT /Zone/example.com", servermock.ResponseFromFixture("publish.json"), - servermock.CheckRequestJSONBody(`{"publish":true,"notes":"my message"}`)). - Build(t) + client := setupTest(t, "/Zone/example.com", unauthenticatedHandler(http.MethodPut, http.StatusOK, "publish.json")) - err := client.Publish(t.Context(), "example.com", "my message") + err := client.Publish(context.Background(), "example.com", "my message") require.NoError(t, err) } func TestClient_AddTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /TXTRecord/example.com/example.com.", servermock.ResponseFromFixture("create-txt-record.json"), - servermock.CheckRequestJSONBody(`{"rdata":{"txtdata":"txt"},"ttl":"120"}`)). - Build(t) + client := setupTest(t, "/TXTRecord/example.com/example.com.", unauthenticatedHandler(http.MethodPost, http.StatusCreated, "create-txt-record.json")) - err := client.AddTXTRecord(t.Context(), "example.com", "example.com.", "txt", 120) + err := client.AddTXTRecord(context.Background(), "example.com", "example.com.", "txt", 120) require.NoError(t, err) } func TestClient_RemoveTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /TXTRecord/example.com/example.com.", nil). - Build(t) + client := setupTest(t, "/TXTRecord/example.com/example.com.", unauthenticatedHandler(http.MethodDelete, http.StatusOK, "")) - err := client.RemoveTXTRecord(t.Context(), "example.com", "example.com.") + err := client.RemoveTXTRecord(context.Background(), "example.com", "example.com.") require.NoError(t, err) } diff --git a/providers/dns/dyn/internal/session.go b/providers/dns/dyn/internal/session.go index 088510152..647080fa8 100644 --- a/providers/dns/dyn/internal/session.go +++ b/providers/dns/dyn/internal/session.go @@ -33,7 +33,6 @@ func (c *Client) login(ctx context.Context) (session, error) { } var s session - err = json.Unmarshal(dynRes.Data, &s) if err != nil { return session{}, errutils.NewUnmarshalError(req, http.StatusOK, dynRes.Data, err) diff --git a/providers/dns/dyn/internal/session_test.go b/providers/dns/dyn/internal/session_test.go index 349b1b190..76d5bef4e 100644 --- a/providers/dns/dyn/internal/session_test.go +++ b/providers/dns/dyn/internal/session_test.go @@ -2,26 +2,21 @@ package internal import ( "context" + "net/http" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockContext(t *testing.T) context.Context { - t.Helper() - - return context.WithValue(t.Context(), tokenKey, "tok") +func mockContext() context.Context { + return context.WithValue(context.Background(), tokenKey, "tok") } func TestClient_login(t *testing.T) { - client := mockBuilder(). - Route("POST /Session", servermock.ResponseFromFixture("login.json"), - servermock.CheckRequestJSONBody(`{"customer_name":"bob","user_name":"user","password":"secret"}`)). - Build(t) + client := setupTest(t, "/Session", unauthenticatedHandler(http.MethodPost, http.StatusOK, "login.json")) - sess, err := client.login(t.Context()) + sess, err := client.login(context.Background()) require.NoError(t, err) expected := session{Token: "tok", Version: "456"} @@ -30,24 +25,16 @@ func TestClient_login(t *testing.T) { } func TestClient_Logout(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient, - servermock.CheckHeader().WithJSONHeaders(). - With(authTokenHeader, "tok"), - ). - Route("DELETE /Session", nil). - Build(t) + client := setupTest(t, "/Session", authenticatedHandler(http.MethodDelete, http.StatusOK, "")) - err := client.Logout(mockContext(t)) + err := client.Logout(mockContext()) require.NoError(t, err) } func TestClient_CreateAuthenticatedContext(t *testing.T) { - client := mockBuilder(). - Route("POST /Session", servermock.ResponseFromFixture("login.json"), - servermock.CheckRequestJSONBody(`{"customer_name":"bob","user_name":"user","password":"secret"}`)). - Build(t) + client := setupTest(t, "/Session", unauthenticatedHandler(http.MethodPost, http.StatusOK, "login.json")) - ctx, err := client.CreateAuthenticatedContext(t.Context()) + ctx, err := client.CreateAuthenticatedContext(context.Background()) require.NoError(t, err) at := getToken(ctx) diff --git a/providers/dns/dyndnsfree/dyndnsfree.go b/providers/dns/dyndnsfree/dyndnsfree.go deleted file mode 100644 index 09be2bfbd..000000000 --- a/providers/dns/dyndnsfree/dyndnsfree.go +++ /dev/null @@ -1,120 +0,0 @@ -// Package dyndnsfree implements a DNS provider for solving the DNS-01 challenge using DynDnsFree.de API. -package dyndnsfree - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/dyndnsfree/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "DYNDNSFREE_" - - EnvUsername = envNamespace + "USERNAME" - EnvPassword = envNamespace + "PASSWORD" - - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - Username string - Password string - - PropagationTimeout time.Duration - PollingInterval time.Duration - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for DynDNSFree. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvUsername, EnvPassword) - if err != nil { - return nil, fmt.Errorf("dyndnsfree: %w", err) - } - - config := NewDefaultConfig() - config.Username = values[EnvUsername] - config.Password = values[EnvPassword] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for DynDNSFree. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("dyndnsfree: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.Username, config.Password) - if err != nil { - return nil, fmt.Errorf("dyndnsfree: new client: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("dyndnsforfree: could not find zone for domain %q: %w", domain, err) - } - - err = d.client.AddTXTRecord(context.Background(), dns01.UnFqdn(authZone), dns01.UnFqdn(info.EffectiveFQDN), info.Value) - if err != nil { - return fmt.Errorf("dyndnsfree: add record: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - // Records are deleted automatically. - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/dyndnsfree/dyndnsfree.toml b/providers/dns/dyndnsfree/dyndnsfree.toml deleted file mode 100644 index e64bb0080..000000000 --- a/providers/dns/dyndnsfree/dyndnsfree.toml +++ /dev/null @@ -1,23 +0,0 @@ -Name = "DynDnsFree.de" -Description = '''''' -URL = "https://www.dyndnsfree.de" -Code = "dyndnsfree" -Since = "v4.23.0" - -Example = ''' -DYNDNSFREE_USERNAME="xxx" \ -DYNDNSFREE_PASSWORD="yyy" \ -lego --dns dyndnsfree -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - DYNDNSFREE_USERNAME = "Username" - DYNDNSFREE_PASSWORD = "Password" - [Configuration.Additional] - DYNDNSFREE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - DYNDNSFREE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - DYNDNSFREE_HTTP_TIMEOUT = "Request timeout in seconds (Default: 30)" - -[Links] - API = "https://www.dyndnsfree.de/user/hilfe.php?hsm=2" diff --git a/providers/dns/dyndnsfree/dyndnsfree_test.go b/providers/dns/dyndnsfree/dyndnsfree_test.go deleted file mode 100644 index 0b03bd27f..000000000 --- a/providers/dns/dyndnsfree/dyndnsfree_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package dyndnsfree - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvUsername, EnvPassword).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvUsername: "user", - EnvPassword: "secret", - }, - }, - { - desc: "missing username", - envVars: map[string]string{ - EnvUsername: "", - EnvPassword: "secret", - }, - expected: "dyndnsfree: some credentials information are missing: DYNDNSFREE_USERNAME", - }, - { - desc: "missing password", - envVars: map[string]string{ - EnvUsername: "user", - EnvPassword: "", - }, - expected: "dyndnsfree: some credentials information are missing: DYNDNSFREE_PASSWORD", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "dyndnsfree: some credentials information are missing: DYNDNSFREE_USERNAME,DYNDNSFREE_PASSWORD", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - username string - password string - expected string - }{ - { - desc: "success", - username: "user", - password: "secret", - }, - { - desc: "missing username", - username: "", - password: "secret", - expected: "dyndnsfree: new client: credentials missing", - }, - { - desc: "missing password", - username: "user", - password: "", - expected: "dyndnsfree: new client: credentials missing", - }, - { - desc: "missing credentials", - expected: "dyndnsfree: new client: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.Username = test.username - config.Password = test.password - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/dyndnsfree/internal/client.go b/providers/dns/dyndnsfree/internal/client.go deleted file mode 100644 index 02a1f1607..000000000 --- a/providers/dns/dyndnsfree/internal/client.go +++ /dev/null @@ -1,78 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "errors" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" -) - -const defaultBaseURL = "https://dynup.de/acme.php" - -type Client struct { - username string - password string - - baseURL string - HTTPClient *http.Client -} - -func NewClient(username, password string) (*Client, error) { - if username == "" || password == "" { - return nil, errors.New("credentials missing") - } - - return &Client{ - username: username, - password: password, - baseURL: defaultBaseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -func (c *Client) AddTXTRecord(ctx context.Context, zone, hostname, value string) error { - baseURL, err := url.Parse(c.baseURL) - if err != nil { - return err - } - - query := baseURL.Query() - query.Set("username", c.username) - query.Set("password", c.password) - query.Set("hostname", zone) - query.Set("add_hostname", hostname) - query.Set("txt", value) - baseURL.RawQuery = query.Encode() - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL.String(), http.NoBody) - if err != nil { - return err - } - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - return errutils.NewUnexpectedResponseStatusCodeError(req, resp) - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - if !bytes.Equal(raw, []byte("success")) { - return errors.New(string(raw)) - } - - return nil -} diff --git a/providers/dns/dyndnsfree/internal/client_test.go b/providers/dns/dyndnsfree/internal/client_test.go deleted file mode 100644 index d6f1d276b..000000000 --- a/providers/dns/dyndnsfree/internal/client_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package internal - -import ( - "net/http/httptest" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -func setupClient(server *httptest.Server) (*Client, error) { - client, err := NewClient("user", "secret") - if err != nil { - return nil, err - } - - client.baseURL = server.URL - client.HTTPClient = server.Client() - - return client, nil -} - -func TestAddTXTRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /", servermock.RawStringResponse("success"), - servermock.CheckQueryParameter().Strict(). - With("add_hostname", "sub.example.com"). - With("hostname", "example.com"). - With("password", "secret"). - With("txt", "value"). - With("username", "user")). - Build(t) - - err := client.AddTXTRecord(t.Context(), "example.com", "sub.example.com", "value") - require.NoError(t, err) -} - -func TestAddTXTRecord_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /", servermock.RawStringResponse("error: authentification failed")). - Build(t) - - err := client.AddTXTRecord(t.Context(), "example.com", "sub.example.com", "value") - require.EqualError(t, err, "error: authentification failed") -} diff --git a/providers/dns/dynu/dynu.go b/providers/dns/dynu/dynu.go index 11df45281..d0c396a2c 100644 --- a/providers/dns/dynu/dynu.go +++ b/providers/dns/dynu/dynu.go @@ -8,11 +8,9 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/dynu/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -27,8 +25,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string @@ -87,8 +83,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { } client := internal.NewClient() - - client.HTTPClient = clientdebug.Wrap(tr.Wrap(config.HTTPClient)) + client.HTTPClient = tr.Wrap(config.HTTPClient) return &DNSProvider{config: config, client: client}, nil } diff --git a/providers/dns/dynu/dynu.toml b/providers/dns/dynu/dynu.toml index ae2367087..22976ef40 100644 --- a/providers/dns/dynu/dynu.toml +++ b/providers/dns/dynu/dynu.toml @@ -6,17 +6,17 @@ Since = "v3.5.0" Example = ''' DYNU_API_KEY=1234567890abcdefghijklmnopqrstuvwxyz \ -lego --dns dynu -d '*.example.com' -d example.com run +lego --email you@example.com --dns dynu --domains my.example.org run ''' [Configuration] [Configuration.Credentials] DYNU_API_KEY = "API key" [Configuration.Additional] - DYNU_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" - DYNU_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 180)" - DYNU_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - DYNU_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + DYNU_POLLING_INTERVAL = "Time between DNS propagation check" + DYNU_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + DYNU_TTL = "The TTL of the TXT record used for the DNS challenge" + DYNU_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.dynu.com/en-US/Support/API" diff --git a/providers/dns/dynu/dynu_test.go b/providers/dns/dynu/dynu_test.go index ffc7c3a00..fe2c22dfb 100644 --- a/providers/dns/dynu/dynu_test.go +++ b/providers/dns/dynu/dynu_test.go @@ -38,7 +38,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -97,7 +96,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -111,7 +109,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/dynu/internal/auth.go b/providers/dns/dynu/internal/auth.go index 0a91445d2..7a21a10e8 100644 --- a/providers/dns/dynu/internal/auth.go +++ b/providers/dns/dynu/internal/auth.go @@ -46,7 +46,6 @@ func (t *TokenTransport) transport() http.RoundTripper { if t.Transport != nil { return t.Transport } - return http.DefaultTransport } diff --git a/providers/dns/dynu/internal/client.go b/providers/dns/dynu/internal/client.go index 59e90d592..6821863b3 100644 --- a/providers/dns/dynu/internal/client.go +++ b/providers/dns/dynu/internal/client.go @@ -12,9 +12,8 @@ import ( "strconv" "time" - "github.com/cenkalti/backoff/v5" + "github.com/cenkalti/backoff/v4" "github.com/go-acme/lego/v4/log" - "github.com/go-acme/lego/v4/platform/wait" "github.com/go-acme/lego/v4/providers/dns/internal/errutils" ) @@ -35,7 +34,7 @@ func NewClient() *Client { } // GetRecords Get DNS records based on a hostname and resource record type. -func (c *Client) GetRecords(ctx context.Context, hostname, recordType string) ([]DNSRecord, error) { +func (c Client) GetRecords(ctx context.Context, hostname, recordType string) ([]DNSRecord, error) { endpoint := c.baseURL.JoinPath("dns", "record", hostname) query := endpoint.Query() @@ -43,7 +42,6 @@ func (c *Client) GetRecords(ctx context.Context, hostname, recordType string) ([ endpoint.RawQuery = query.Encode() apiResp := RecordsResponse{} - err := c.doRetry(ctx, http.MethodGet, endpoint.String(), nil, &apiResp) if err != nil { return nil, err @@ -57,7 +55,7 @@ func (c *Client) GetRecords(ctx context.Context, hostname, recordType string) ([ } // AddNewRecord Add a new DNS record for DNS service. -func (c *Client) AddNewRecord(ctx context.Context, domainID int64, record DNSRecord) error { +func (c Client) AddNewRecord(ctx context.Context, domainID int64, record DNSRecord) error { endpoint := c.baseURL.JoinPath("dns", strconv.FormatInt(domainID, 10), "record") reqBody, err := json.Marshal(record) @@ -66,7 +64,6 @@ func (c *Client) AddNewRecord(ctx context.Context, domainID int64, record DNSRec } apiResp := RecordResponse{} - err = c.doRetry(ctx, http.MethodPost, endpoint.String(), reqBody, &apiResp) if err != nil { return err @@ -80,11 +77,10 @@ func (c *Client) AddNewRecord(ctx context.Context, domainID int64, record DNSRec } // DeleteRecord Remove a DNS record from DNS service. -func (c *Client) DeleteRecord(ctx context.Context, domainID, recordID int64) error { +func (c Client) DeleteRecord(ctx context.Context, domainID, recordID int64) error { endpoint := c.baseURL.JoinPath("dns", strconv.FormatInt(domainID, 10), "record", strconv.FormatInt(recordID, 10)) apiResp := APIException{} - err := c.doRetry(ctx, http.MethodDelete, endpoint.String(), nil, &apiResp) if err != nil { return err @@ -98,11 +94,10 @@ func (c *Client) DeleteRecord(ctx context.Context, domainID, recordID int64) err } // GetRootDomain Get the root domain name based on a hostname. -func (c *Client) GetRootDomain(ctx context.Context, hostname string) (*DNSHostname, error) { +func (c Client) GetRootDomain(ctx context.Context, hostname string) (*DNSHostname, error) { endpoint := c.baseURL.JoinPath("dns", "getroot", hostname) apiResp := DNSHostname{} - err := c.doRetry(ctx, http.MethodGet, endpoint.String(), nil, &apiResp) if err != nil { return nil, err @@ -116,7 +111,7 @@ func (c *Client) GetRootDomain(ctx context.Context, hostname string) (*DNSHostna } // doRetry the API is really unstable, so we need to retry on EOF. -func (c *Client) doRetry(ctx context.Context, method, uri string, body []byte, result any) error { +func (c Client) doRetry(ctx context.Context, method, uri string, body []byte, result any) error { operation := func() error { return c.do(ctx, method, uri, body, result) } @@ -128,10 +123,15 @@ func (c *Client) doRetry(ctx context.Context, method, uri string, body []byte, r bo := backoff.NewExponentialBackOff() bo.InitialInterval = 1 * time.Second - return wait.Retry(ctx, operation, backoff.WithBackOff(bo), backoff.WithNotify(notify)) + err := backoff.RetryNotify(operation, bo, notify) + if err != nil { + return err + } + + return nil } -func (c *Client) do(ctx context.Context, method, uri string, body []byte, result any) error { +func (c Client) do(ctx context.Context, method, uri string, body []byte, result any) error { var reqBody io.Reader if len(body) > 0 { reqBody = bytes.NewReader(body) diff --git a/providers/dns/dynu/internal/client_test.go b/providers/dns/dynu/internal/client_test.go index f70a8e377..6819310f7 100644 --- a/providers/dns/dynu/internal/client_test.go +++ b/providers/dns/dynu/internal/client_test.go @@ -1,27 +1,53 @@ package internal import ( + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient() - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) +func setupTest(t *testing.T, method, pattern string, status int, file string) *Client { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(), - ) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + open, err := os.Open(file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = open.Close() }() + + rw.WriteHeader(status) + _, err = io.Copy(rw, open) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + client := NewClient() + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client } func TestGetRootDomain(t *testing.T) { @@ -39,9 +65,9 @@ func TestGetRootDomain(t *testing.T) { }{ { desc: "success", - pattern: "GET /dns/getroot/test.lego.freeddns.org", + pattern: "/dns/getroot/test.lego.freeddns.org", status: http.StatusOK, - file: "get_root_domain.json", + file: "./fixtures/get_root_domain.json", expected: expected{ domain: &DNSHostname{ APIException: &APIException{ @@ -56,9 +82,9 @@ func TestGetRootDomain(t *testing.T) { }, { desc: "invalid", - pattern: "GET /dns/getroot/test.lego.freeddns.org", + pattern: "/dns/getroot/test.lego.freeddns.org", status: http.StatusNotImplemented, - file: "get_root_domain_invalid.json", + file: "./fixtures/get_root_domain_invalid.json", expected: expected{ error: "API error: 501: Argument Exception: Invalid.", }, @@ -69,11 +95,9 @@ func TestGetRootDomain(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - client := mockBuilder(). - Route(test.pattern, servermock.ResponseFromFixture(test.file).WithStatusCode(test.status)). - Build(t) + client := setupTest(t, http.MethodGet, test.pattern, test.status, test.file) - domain, err := client.GetRootDomain(t.Context(), "test.lego.freeddns.org") + domain, err := client.GetRootDomain(context.Background(), "test.lego.freeddns.org") if test.expected.error != "" { assert.EqualError(t, err, test.expected.error) @@ -82,7 +106,7 @@ func TestGetRootDomain(t *testing.T) { require.NoError(t, err) - assert.NotNil(t, domain) + assert.NotNil(t, domain) //nolint:testifylint // false positive https://github.com/Antonboom/testifylint/issues/95 assert.Equal(t, test.expected.domain, domain) }) } @@ -103,9 +127,9 @@ func TestGetRecords(t *testing.T) { }{ { desc: "success", - pattern: "GET /dns/record/_acme-challenge.lego.freeddns.org", + pattern: "/dns/record/_acme-challenge.lego.freeddns.org", status: http.StatusOK, - file: "get_records.json", + file: "./fixtures/get_records.json", expected: expected{ records: []DNSRecord{ { @@ -137,18 +161,18 @@ func TestGetRecords(t *testing.T) { }, { desc: "empty", - pattern: "GET /dns/record/_acme-challenge.lego.freeddns.org", + pattern: "/dns/record/_acme-challenge.lego.freeddns.org", status: http.StatusOK, - file: "get_records_empty.json", + file: "./fixtures/get_records_empty.json", expected: expected{ records: []DNSRecord{}, }, }, { desc: "invalid", - pattern: "GET /dns/record/_acme-challenge.lego.freeddns.org", + pattern: "/dns/record/_acme-challenge.lego.freeddns.org", status: http.StatusNotImplemented, - file: "get_records_invalid.json", + file: "./fixtures/get_records_invalid.json", expected: expected{ error: "API error: 501: Argument Exception: Invalid.", }, @@ -159,13 +183,9 @@ func TestGetRecords(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - client := mockBuilder(). - Route(test.pattern, servermock.ResponseFromFixture(test.file).WithStatusCode(test.status), - servermock.CheckQueryParameter().Strict(). - With("recordType", "TXT")). - Build(t) + client := setupTest(t, http.MethodGet, test.pattern, test.status, test.file) - records, err := client.GetRecords(t.Context(), "_acme-challenge.lego.freeddns.org", "TXT") + records, err := client.GetRecords(context.Background(), "_acme-challenge.lego.freeddns.org", "TXT") if test.expected.error != "" { assert.EqualError(t, err, test.expected.error) @@ -194,15 +214,15 @@ func TestAddNewRecord(t *testing.T) { }{ { desc: "success", - pattern: "POST /dns/9007481/record", + pattern: "/dns/9007481/record", status: http.StatusOK, - file: "add_new_record.json", + file: "./fixtures/add_new_record.json", }, { desc: "invalid", - pattern: "POST /dns/9007481/record", + pattern: "/dns/9007481/record", status: http.StatusNotImplemented, - file: "add_new_record_invalid.json", + file: "./fixtures/add_new_record_invalid.json", expected: expected{ error: "API error: 501: Argument Exception: Invalid.", }, @@ -213,10 +233,7 @@ func TestAddNewRecord(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - client := mockBuilder(). - Route(test.pattern, servermock.ResponseFromFixture(test.file).WithStatusCode(test.status), - servermock.CheckRequestJSONBodyFromFixture("add_new_record-request.json")). - Build(t) + client := setupTest(t, http.MethodPost, test.pattern, test.status, test.file) record := DNSRecord{ Type: "TXT", @@ -228,7 +245,7 @@ func TestAddNewRecord(t *testing.T) { TTL: 300, } - err := client.AddNewRecord(t.Context(), 9007481, record) + err := client.AddNewRecord(context.Background(), 9007481, record) if test.expected.error != "" { assert.EqualError(t, err, test.expected.error) @@ -254,15 +271,15 @@ func TestDeleteRecord(t *testing.T) { }{ { desc: "success", - pattern: "DELETE /", + pattern: "/", status: http.StatusOK, - file: "delete_record.json", + file: "./fixtures/delete_record.json", }, { desc: "invalid", - pattern: "DELETE /", + pattern: "/", status: http.StatusNotImplemented, - file: "delete_record_invalid.json", + file: "./fixtures/delete_record_invalid.json", expected: expected{ error: "API error: 501: Argument Exception: Invalid.", }, @@ -273,11 +290,9 @@ func TestDeleteRecord(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - client := mockBuilder(). - Route(test.pattern, servermock.ResponseFromFixture(test.file).WithStatusCode(test.status)). - Build(t) + client := setupTest(t, http.MethodDelete, test.pattern, test.status, test.file) - err := client.DeleteRecord(t.Context(), 9007481, 6041418) + err := client.DeleteRecord(context.Background(), 9007481, 6041418) if test.expected.error != "" { assert.EqualError(t, err, test.expected.error) diff --git a/providers/dns/dynu/internal/fixtures/add_new_record-request.json b/providers/dns/dynu/internal/fixtures/add_new_record-request.json deleted file mode 100644 index f3c75ca36..000000000 --- a/providers/dns/dynu/internal/fixtures/add_new_record-request.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "recordType": "TXT", - "domainName": "lego.freeddns.org", - "nodeName": "_acme-challenge", - "hostname": "_acme-challenge.lego.freeddns.org", - "state": true, - "textData": "txt_txt_txt_txt_txt_txt_txt_2", - "ttl": 300 -} diff --git a/providers/dns/easydns/easydns.go b/providers/dns/easydns/easydns.go index 205063e7b..20ad27543 100644 --- a/providers/dns/easydns/easydns.go +++ b/providers/dns/easydns/easydns.go @@ -12,11 +12,9 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/easydns/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -34,8 +32,6 @@ const ( EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Endpoint *url.URL @@ -78,7 +74,6 @@ func NewDNSProvider() (*DNSProvider, error) { if err != nil { return nil, fmt.Errorf("easydns: %w", err) } - config.Endpoint = endpoint values, err := env.Get(EnvToken, EnvKey) @@ -112,8 +107,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - if config.Endpoint != nil { client.BaseURL = config.Endpoint } @@ -190,14 +183,15 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } err = d.client.DeleteRecord(ctx, dns01.UnFqdn(authZone), recordID) + + d.recordIDsMu.Lock() + defer delete(d.recordIDs, key) + d.recordIDsMu.Unlock() + if err != nil { return fmt.Errorf("easydns: %w", err) } - d.recordIDsMu.Lock() - delete(d.recordIDs, key) - d.recordIDsMu.Unlock() - return nil } diff --git a/providers/dns/easydns/easydns.toml b/providers/dns/easydns/easydns.toml index 307c86a09..0b243f276 100644 --- a/providers/dns/easydns/easydns.toml +++ b/providers/dns/easydns/easydns.toml @@ -5,9 +5,9 @@ Code = "easydns" Since = "v2.6.0" Example = ''' -EASYDNS_TOKEN=xxx \ -EASYDNS_KEY=yyy \ -lego --dns easydns -d '*.example.com' -d example.com run +EASYDNS_TOKEN= \ +EASYDNS_KEY= \ +lego --email you@example.com --dns easydns --domains my.example.org run ''' Additional = ''' @@ -20,11 +20,11 @@ To test with the sandbox environment set ```EASYDNS_ENDPOINT=https://sandbox.res EASYDNS_KEY = "API Key" [Configuration.Additional] EASYDNS_ENDPOINT = "The endpoint URL of the API Server" - EASYDNS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - EASYDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - EASYDNS_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 60)" - EASYDNS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - EASYDNS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + EASYDNS_POLLING_INTERVAL = "Time between DNS propagation check" + EASYDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + EASYDNS_SEQUENCE_INTERVAL = "Time between sequential requests" + EASYDNS_TTL = "The TTL of the TXT record used for the DNS challenge" + EASYDNS_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://docs.sandbox.rest.easydns.net" diff --git a/providers/dns/easydns/easydns_test.go b/providers/dns/easydns/easydns_test.go index 5517928d7..972ff8cda 100644 --- a/providers/dns/easydns/easydns_test.go +++ b/providers/dns/easydns/easydns_test.go @@ -2,6 +2,7 @@ package easydns import ( "fmt" + "io" "net/http" "net/http/httptest" "net/url" @@ -9,10 +10,12 @@ import ( "time" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +const authorizationHeader = "Authorization" + const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( @@ -21,27 +24,26 @@ var envTest = tester.NewEnvTest( EnvKey). WithDomain(envDomain) -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - endpoint, err := url.Parse(server.URL) - if err != nil { - return nil, err - } +func setupTest(t *testing.T) (*DNSProvider, *http.ServeMux) { + t.Helper() - config := NewDefaultConfig() - config.Token = "TOKEN" - config.Key = "SECRET" - config.Endpoint = endpoint - config.HTTPClient = server.Client() + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) - return NewDNSProviderConfig(config) - }, - servermock.CheckHeader(). - WithJSONHeaders(). - WithAuthorization("Basic VE9LRU46U0VDUkVU"), - servermock.CheckQueryParameter().Strict(). - With("format", "json")) + endpoint, err := url.Parse(server.URL) + require.NoError(t, err) + + config := NewDefaultConfig() + config.Token = "TOKEN" + config.Key = "SECRET" + config.Endpoint = endpoint + config.HTTPClient = server.Client() + + provider, err := NewDNSProviderConfig(config) + require.NoError(t, err) + + return provider, mux } func TestNewDNSProvider(t *testing.T) { @@ -76,7 +78,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -144,138 +145,15 @@ func TestNewDNSProviderConfig(t *testing.T) { } func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("GET /zones/records/all/example.com", - servermock.RawStringResponse(`{ - "msg": "string", - "status": 200, - "tm": 0, - "data": [{ - "id": "60898922", - "domain": "example.com", - "host": "hosta", - "ttl": "300", - "prio": "0", - "geozone_id": "0", - "type": "A", - "rdata": "1.2.3.4", - "last_mod": "2019-08-28 19:09:50" - }], - "count": 0, - "total": 0, - "start": 0, - "max": 0 - } - `), - servermock.CheckQueryParameter().Strict(). - With("format", "json")). - Route("PUT /zones/records/add/example.com/TXT", - servermock.RawStringResponse(`{ - "msg": "OK", - "tm": 1554681934, - "data": { - "host": "_acme-challenge", - "geozone_id": 0, - "ttl": "120", - "prio": "0", - "rdata": "pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM", - "revoked": 0, - "id": "123456789", - "new_host": "_acme-challenge.example.com" - }, - "status": 201 - }`), - servermock.CheckRequestJSONBody(`{"domain":"example.com","host":"_acme-challenge","ttl":"120","prio":"0","type":"TXT","rdata":"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM"} -`)). - Build(t) + provider, mux := setupTest(t) - err := provider.Present("example.com", "token", "keyAuth") - require.NoError(t, err) - require.Contains(t, provider.recordIDs, "_acme-challenge.example.com.|pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM") -} + mux.HandleFunc("/zones/records/all/example.com", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method, "method") + assert.Equal(t, "format=json", r.URL.RawQuery, "query") + assert.Equal(t, "Basic VE9LRU46U0VDUkVU", r.Header.Get(authorizationHeader), authorizationHeader) -func TestDNSProvider_Cleanup_WhenRecordIdNotSet_NoOp(t *testing.T) { - provider := mockBuilder(). - Route("GET /zones/records/all/_acme-challenge.example.com", - servermock.RawStringResponse(`{ - "msg": "string", - "status": 200, - "tm": 0, - "data": [{ - "id": "60898922", - "domain": "example.com", - "host": "hosta", - "ttl": "300", - "prio": "0", - "geozone_id": "0", - "type": "A", - "rdata": "1.2.3.4", - "last_mod": "2019-08-28 19:09:50" - }], - "count": 0, - "total": 0, - "start": 0, - "max": 0 - } - `)). - Build(t) - - err := provider.CleanUp("example.com", "token", "keyAuth") - require.NoError(t, err) -} - -func TestDNSProvider_Cleanup_WhenRecordIdSet_DeletesTxtRecord(t *testing.T) { - provider := mockBuilder(). - Route("GET /zones/records/all/_acme-challenge.example.com", - servermock.RawStringResponse(`{ - "msg": "string", - "status": 200, - "tm": 0, - "data": [{ - "id": "60898922", - "domain": "example.com", - "host": "hosta", - "ttl": "300", - "prio": "0", - "geozone_id": "0", - "type": "A", - "rdata": "1.2.3.4", - "last_mod": "2019-08-28 19:09:50" - }], - "count": 0, - "total": 0, - "start": 0, - "max": 0 - } - `)). - Route("DELETE /zones/records/_acme-challenge.example.com/123456", - servermock.RawStringResponse(`{ - "msg": "OK", - "data": { - "domain": "example.com", - "id": "123456" - }, - "status": 200 - }`)). - Build(t) - - provider.recordIDs["_acme-challenge.example.com.|pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM"] = "123456" - - err := provider.CleanUp("example.com", "token", "keyAuth") - require.NoError(t, err) -} - -func TestDNSProvider_Cleanup_WhenHttpError_ReturnsError(t *testing.T) { - errorMessage := `{ - "error": { - "code": 406, - "message": "Provided id is invalid or you do not have permission to access it." - } - }` - - provider := mockBuilder(). - Route("GET /zones/records/all/example.com", - servermock.RawStringResponse(`{ + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprintf(w, `{ "msg": "string", "status": 200, "tm": 0, @@ -295,16 +173,214 @@ func TestDNSProvider_Cleanup_WhenHttpError_ReturnsError(t *testing.T) { "start": 0, "max": 0 } -`)). - Route("DELETE /zones/records/example.com/123456", - servermock.RawStringResponse(errorMessage). - WithStatusCode(http.StatusNotAcceptable)). - Build(t) +`) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) - provider.recordIDs["_acme-challenge.example.com.|pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM"] = "123456" + mux.HandleFunc("/zones/records/add/example.com/TXT", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPut, r.Method, "method") + assert.Equal(t, "format=json", r.URL.RawQuery, "query") + assert.Equal(t, "application/json", r.Header.Get("Content-Type"), "Content-Type") + assert.Equal(t, "Basic VE9LRU46U0VDUkVU", r.Header.Get(authorizationHeader), authorizationHeader) + + reqBody, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + expectedReqBody := `{"domain":"example.com","host":"_acme-challenge","ttl":"120","prio":"0","type":"TXT","rdata":"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM"} +` + assert.Equal(t, expectedReqBody, string(reqBody)) + + w.WriteHeader(http.StatusCreated) + _, err = fmt.Fprintf(w, `{ + "msg": "OK", + "tm": 1554681934, + "data": { + "host": "_acme-challenge", + "geozone_id": 0, + "ttl": "120", + "prio": "0", + "rdata": "pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM", + "revoked": 0, + "id": "123456789", + "new_host": "_acme-challenge.example.com" + }, + "status": 201 + }`) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + err := provider.Present("example.com", "token", "keyAuth") + require.NoError(t, err) + require.Contains(t, provider.recordIDs, "_acme-challenge.example.com.|pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM") +} + +func TestDNSProvider_Cleanup_WhenRecordIdNotSet_NoOp(t *testing.T) { + provider, mux := setupTest(t) + + mux.HandleFunc("/zones/records/all/example.com", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method, "method") + assert.Equal(t, "format=json", r.URL.RawQuery, "query") + assert.Equal(t, "Basic VE9LRU46U0VDUkVU", r.Header.Get(authorizationHeader), authorizationHeader) + + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprintf(w, `{ + "msg": "string", + "status": 200, + "tm": 0, + "data": [{ + "id": "60898922", + "domain": "example.com", + "host": "hosta", + "ttl": "300", + "prio": "0", + "geozone_id": "0", + "type": "A", + "rdata": "1.2.3.4", + "last_mod": "2019-08-28 19:09:50" + }], + "count": 0, + "total": 0, + "start": 0, + "max": 0 +} +`) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) err := provider.CleanUp("example.com", "token", "keyAuth") + require.NoError(t, err) +} +func TestDNSProvider_Cleanup_WhenRecordIdSet_DeletesTxtRecord(t *testing.T) { + provider, mux := setupTest(t) + + mux.HandleFunc("/zones/records/all/example.com", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method, "method") + assert.Equal(t, "format=json", r.URL.RawQuery, "query") + assert.Equal(t, "Basic VE9LRU46U0VDUkVU", r.Header.Get(authorizationHeader), authorizationHeader) + + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprintf(w, `{ + "msg": "string", + "status": 200, + "tm": 0, + "data": [{ + "id": "60898922", + "domain": "example.com", + "host": "hosta", + "ttl": "300", + "prio": "0", + "geozone_id": "0", + "type": "A", + "rdata": "1.2.3.4", + "last_mod": "2019-08-28 19:09:50" + }], + "count": 0, + "total": 0, + "start": 0, + "max": 0 +} +`) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + mux.HandleFunc("/zones/records/example.com/123456", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodDelete, r.Method, "method") + assert.Equal(t, "format=json", r.URL.RawQuery, "query") + assert.Equal(t, "Basic VE9LRU46U0VDUkVU", r.Header.Get(authorizationHeader), authorizationHeader) + + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprintf(w, `{ + "msg": "OK", + "data": { + "domain": "example.com", + "id": "123456" + }, + "status": 200 + }`) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + provider.recordIDs["_acme-challenge.example.com.|pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM"] = "123456" + err := provider.CleanUp("example.com", "token", "keyAuth") + require.NoError(t, err) +} + +func TestDNSProvider_Cleanup_WhenHttpError_ReturnsError(t *testing.T) { + provider, mux := setupTest(t) + + mux.HandleFunc("/zones/records/all/example.com", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method, "method") + assert.Equal(t, "format=json", r.URL.RawQuery, "query") + assert.Equal(t, "Basic VE9LRU46U0VDUkVU", r.Header.Get(authorizationHeader), authorizationHeader) + + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprintf(w, `{ + "msg": "string", + "status": 200, + "tm": 0, + "data": [{ + "id": "60898922", + "domain": "example.com", + "host": "hosta", + "ttl": "300", + "prio": "0", + "geozone_id": "0", + "type": "A", + "rdata": "1.2.3.4", + "last_mod": "2019-08-28 19:09:50" + }], + "count": 0, + "total": 0, + "start": 0, + "max": 0 +} +`) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + errorMessage := `{ + "error": { + "code": 406, + "message": "Provided id is invalid or you do not have permission to access it." + } + }` + mux.HandleFunc("/zones/records/example.com/123456", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodDelete, r.Method, "method") + assert.Equal(t, "format=json", r.URL.RawQuery, "query") + assert.Equal(t, "Basic VE9LRU46U0VDUkVU", r.Header.Get(authorizationHeader), authorizationHeader) + + w.WriteHeader(http.StatusNotAcceptable) + _, err := fmt.Fprint(w, errorMessage) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + provider.recordIDs["_acme-challenge.example.com.|pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM"] = "123456" + err := provider.CleanUp("example.com", "token", "keyAuth") expectedError := fmt.Sprintf("easydns: unexpected status code: [status code: 406] body: %v", errorMessage) require.EqualError(t, err, expectedError) } @@ -315,7 +391,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -329,7 +404,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/easydns/internal/client.go b/providers/dns/easydns/internal/client.go index 33d7c724e..3568eeea5 100644 --- a/providers/dns/easydns/internal/client.go +++ b/providers/dns/easydns/internal/client.go @@ -26,7 +26,7 @@ type Client struct { } // NewClient Creates a new Client. -func NewClient(token, key string) *Client { +func NewClient(token string, key string) *Client { baseURL, _ := url.Parse(DefaultBaseURL) return &Client{ @@ -46,7 +46,6 @@ func (c *Client) ListZones(ctx context.Context, domain string) ([]ZoneRecord, er } response := &apiResponse[[]ZoneRecord]{} - err = c.do(req, response) if err != nil { return nil, err @@ -68,7 +67,6 @@ func (c *Client) AddRecord(ctx context.Context, domain string, record ZoneRecord } response := &apiResponse[*ZoneRecord]{} - err = c.do(req, response) if err != nil { return "", err diff --git a/providers/dns/easydns/internal/client_test.go b/providers/dns/easydns/internal/client_test.go index bf4e1e45b..030b28f34 100644 --- a/providers/dns/easydns/internal/client_test.go +++ b/providers/dns/easydns/internal/client_test.go @@ -1,36 +1,76 @@ package internal import ( + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("tok", "k") - client.HTTPClient = server.Client() - client.BaseURL, _ = url.Parse(server.URL) +func setupTest(t *testing.T, method, pattern string, status int, file string) *Client { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithBasicAuth("tok", "k"), - ) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest) + return + } + + token, key, ok := req.BasicAuth() + if token != "tok" || key != "k" || !ok { + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + if req.URL.Query().Get("format") != "json" { + http.Error(rw, fmt.Sprintf("invalid format: %s", req.URL.Query().Get("format")), http.StatusBadRequest) + return + } + + if file == "" { + rw.WriteHeader(status) + return + } + + open, err := os.Open(filepath.Join("fixtures", file)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = open.Close() }() + + rw.WriteHeader(status) + _, err = io.Copy(rw, open) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + client := NewClient("tok", "k") + client.HTTPClient = server.Client() + client.BaseURL, _ = url.Parse(server.URL) + + return client } func TestClient_ListZones(t *testing.T) { - client := mockBuilder(). - Route("GET /zones/records/all/example.com", servermock.ResponseFromFixture("list-zone.json")). - Build(t) + client := setupTest(t, http.MethodGet, "/zones/records/all/example.com", http.StatusOK, "list-zone.json") - zones, err := client.ListZones(t.Context(), "example.com") + zones, err := client.ListZones(context.Background(), "example.com") require.NoError(t, err) expected := []ZoneRecord{{ @@ -48,20 +88,14 @@ func TestClient_ListZones(t *testing.T) { } func TestClient_ListZones_error(t *testing.T) { - client := mockBuilder(). - Route("GET /zones/records/all/example.com", servermock.ResponseFromFixture("error1.json")). - Build(t) + client := setupTest(t, http.MethodGet, "/zones/records/all/example.com", http.StatusOK, "error1.json") - _, err := client.ListZones(t.Context(), "example.com") + _, err := client.ListZones(context.Background(), "example.com") require.EqualError(t, err, "code 420: Enhance Your Calm. Rate limit exceeded (too many requests) OR you did NOT provide any credentials with your request!") } func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("PUT /zones/records/add/example.com/TXT", - servermock.ResponseFromFixture("add-record.json").WithStatusCode(http.StatusCreated), - servermock.CheckRequestJSONBody(`{"domain":"example.com","host":"test631","ttl":"300","prio":"0","type":"TXT","rdata":"txt"}`)). - Build(t) + client := setupTest(t, http.MethodPut, "/zones/records/add/example.com/TXT", http.StatusCreated, "add-record.json") record := ZoneRecord{ Domain: "example.com", @@ -72,17 +106,14 @@ func TestClient_AddRecord(t *testing.T) { Priority: "0", } - recordID, err := client.AddRecord(t.Context(), "example.com", record) + recordID, err := client.AddRecord(context.Background(), "example.com", record) require.NoError(t, err) assert.Equal(t, "xxx", recordID) } func TestClient_AddRecord_error(t *testing.T) { - client := mockBuilder(). - Route("PUT /zones/records/add/example.com/TXT", - servermock.ResponseFromFixture("error1.json").WithStatusCode(http.StatusCreated)). - Build(t) + client := setupTest(t, http.MethodPut, "/zones/records/add/example.com/TXT", http.StatusCreated, "error1.json") record := ZoneRecord{ Domain: "example.com", @@ -93,15 +124,13 @@ func TestClient_AddRecord_error(t *testing.T) { Priority: "0", } - _, err := client.AddRecord(t.Context(), "example.com", record) + _, err := client.AddRecord(context.Background(), "example.com", record) require.EqualError(t, err, "code 420: Enhance Your Calm. Rate limit exceeded (too many requests) OR you did NOT provide any credentials with your request!") } func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /zones/records/example.com/xxx", nil). - Build(t) + client := setupTest(t, http.MethodDelete, "/zones/records/example.com/xxx", http.StatusOK, "") - err := client.DeleteRecord(t.Context(), "example.com", "xxx") + err := client.DeleteRecord(context.Background(), "example.com", "xxx") require.NoError(t, err) } diff --git a/providers/dns/edgecenter/edgecenter.go b/providers/dns/edgecenter/edgecenter.go deleted file mode 100644 index cfc75b521..000000000 --- a/providers/dns/edgecenter/edgecenter.go +++ /dev/null @@ -1,103 +0,0 @@ -// Package edgecenter implements a DNS provider for solving the DNS-01 challenge using EdgeCenter. -package edgecenter - -import ( - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/gcore" -) - -// Environment variables names. -const ( - envNamespace = "EDGECENTER_" - - EnvPermanentAPIToken = envNamespace + "PERMANENT_API_TOKEN" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -const defaultBaseURL = "https://api.edgecenter.ru/dns" - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config for DNSProvider. -type Config = gcore.Config - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, gcore.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, gcore.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), - }, - } -} - -// DNSProvider an implementation of challenge.Provider contract. -type DNSProvider struct { - prv challenge.ProviderTimeout -} - -// NewDNSProvider returns an instance of DNSProvider configured for G-Core DNS API. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvPermanentAPIToken) - if err != nil { - return nil, fmt.Errorf("edgecenter: %w", err) - } - - config := NewDefaultConfig() - config.APIToken = values[EnvPermanentAPIToken] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for G-Core DNS API. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("edgecenter: the configuration of the DNS provider is nil") - } - - provider, err := gcore.NewDNSProviderConfig(config, defaultBaseURL) - if err != nil { - return nil, fmt.Errorf("edgecenter: %w", err) - } - - return &DNSProvider{prv: provider}, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - err := d.prv.Present(domain, token, keyAuth) - if err != nil { - return fmt.Errorf("edgecenter: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - err := d.prv.CleanUp(domain, token, keyAuth) - if err != nil { - return fmt.Errorf("edgecenter: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.prv.Timeout() -} diff --git a/providers/dns/edgecenter/edgecenter.toml b/providers/dns/edgecenter/edgecenter.toml deleted file mode 100644 index 1c9e9b2a9..000000000 --- a/providers/dns/edgecenter/edgecenter.toml +++ /dev/null @@ -1,22 +0,0 @@ -Name = "EdgeCenter" -Description = '''''' -URL = "https://edgecenter.ru/dns" -Code = "edgecenter" -Since = "v4.29.0" - -Example = ''' -EDGECENTER_PERMANENT_API_TOKEN=xxxxx \ -lego --dns edgecenter -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - EDGECENTER_PERMANENT_API_TOKEN = "Permanent API token (https://edgecenter.ru/blog/permanent-api-token-explained/)" - [Configuration.Additional] - EDGECENTER_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 20)" - EDGECENTER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 360)" - EDGECENTER_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - EDGECENTER_HTTP_TIMEOUT = "API request timeout in seconds (Default: 10)" - -[Links] - API = "https://apidocs.edgecenter.ru/dns" diff --git a/providers/dns/edgecenter/edgecenter_test.go b/providers/dns/edgecenter/edgecenter_test.go deleted file mode 100644 index e3ec43981..000000000 --- a/providers/dns/edgecenter/edgecenter_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package edgecenter - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -var envTest = tester.NewEnvTest(EnvPermanentAPIToken).WithDomain(envNamespace + "DOMAIN") - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvPermanentAPIToken: "A", - }, - }, - { - desc: "missing credentials", - envVars: map[string]string{ - EnvPermanentAPIToken: "", - }, - expected: "edgecenter: some credentials information are missing: EDGECENTER_PERMANENT_API_TOKEN", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.prv) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiToken string - expected string - }{ - { - desc: "success", - apiToken: "A", - }, - { - desc: "missing credentials", - expected: "edgecenter: incomplete credentials provided", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIToken = test.apiToken - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.prv) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/edgedns/edgedns.go b/providers/dns/edgedns/edgedns.go index b5f4b99c9..263ba0c39 100644 --- a/providers/dns/edgedns/edgedns.go +++ b/providers/dns/edgedns/edgedns.go @@ -2,18 +2,14 @@ package edgedns import ( - "context" "errors" "fmt" - "net/http" "slices" "strings" "time" - edgegriddns "github.com/akamai/AkamaiOPEN-edgegrid-golang/v11/pkg/dns" - "github.com/akamai/AkamaiOPEN-edgegrid-golang/v11/pkg/edgegrid" - "github.com/akamai/AkamaiOPEN-edgegrid-golang/v11/pkg/session" - "github.com/go-acme/lego/v4/challenge" + configdns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" @@ -23,22 +19,17 @@ import ( const ( envNamespace = "AKAMAI_" - EnvEdgeRc = envNamespace + "EDGERC" - EnvEdgeRcSection = envNamespace + "EDGERC_SECTION" - EnvAccountSwitchKey = envNamespace + "ACCOUNT_SWITCH_KEY" + EnvEdgeRc = envNamespace + "EDGERC" + EnvEdgeRcSection = envNamespace + "EDGERC_SECTION" - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" -) - -// Test Environment variables names (unused). -// TODO(ldez): must be moved into test files. -const ( EnvHost = envNamespace + "HOST" EnvClientToken = envNamespace + "CLIENT_TOKEN" EnvClientSecret = envNamespace + "CLIENT_SECRET" EnvAccessToken = envNamespace + "ACCESS_TOKEN" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) const ( @@ -48,12 +39,9 @@ const ( const maxBody = 131072 -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { - *edgegrid.Config - + edgegrid.Config PropagationTimeout time.Duration PollingInterval time.Duration TTL int @@ -65,7 +53,7 @@ func NewDefaultConfig() *Config { TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, defaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, defaultPollInterval), - Config: &edgegrid.Config{MaxBody: maxBody}, + Config: edgegrid.Config{MaxBody: maxBody}, } } @@ -80,27 +68,22 @@ type DNSProvider struct { // 1. Section-specific environment variables `AKAMAI_{SECTION}_HOST`, `AKAMAI_{SECTION}_ACCESS_TOKEN`, `AKAMAI_{SECTION}_CLIENT_TOKEN`, `AKAMAI_{SECTION}_CLIENT_SECRET` where `{SECTION}` is specified using `AKAMAI_EDGERC_SECTION` // 2. If `AKAMAI_EDGERC_SECTION` is not defined or is set to `default`: Environment variables `AKAMAI_HOST`, `AKAMAI_ACCESS_TOKEN`, `AKAMAI_CLIENT_TOKEN`, `AKAMAI_CLIENT_SECRET` // 3. .edgerc file located at `AKAMAI_EDGERC` (defaults to `~/.edgerc`, sections can be specified using `AKAMAI_EDGERC_SECTION`) +// 4. Default environment variables: `AKAMAI_HOST`, `AKAMAI_ACCESS_TOKEN`, `AKAMAI_CLIENT_TOKEN`, `AKAMAI_CLIENT_SECRET` // // See also: https://developer.akamai.com/api/getting-started func NewDNSProvider() (*DNSProvider, error) { - conf, err := edgegrid.New( - edgegrid.WithEnv(true), - edgegrid.WithFile(env.GetOrDefaultString(EnvEdgeRc, "~/.edgerc")), - edgegrid.WithSection(env.GetOrDefaultString(EnvEdgeRcSection, "default")), - ) + config := NewDefaultConfig() + + rcPath := env.GetOrDefaultString(EnvEdgeRc, "") + rcSection := env.GetOrDefaultString(EnvEdgeRcSection, "") + + conf, err := edgegrid.Init(rcPath, rcSection) if err != nil { return nil, fmt.Errorf("edgedns: %w", err) } conf.MaxBody = maxBody - accountSwitchKey := env.GetOrDefaultString(EnvAccountSwitchKey, "") - - if accountSwitchKey != "" { - conf.AccountKey = accountSwitchKey - } - - config := NewDefaultConfig() config.Config = conf return NewDNSProviderConfig(config) @@ -112,10 +95,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("edgedns: the configuration of the DNS provider is nil") } - err := config.Validate() - if err != nil { - return nil, fmt.Errorf("edgedns: %w", err) - } + configdns.Init(config.Config) return &DNSProvider{config: config}, nil } @@ -128,27 +108,14 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) - sess, err := session.New(session.WithSigner(d.config)) - if err != nil { - return fmt.Errorf("edgedns: %w", err) - } - - client := edgegriddns.Client(sess) - zone, err := getZone(info.EffectiveFQDN) if err != nil { return fmt.Errorf("edgedns: %w", err) } - record, err := client.GetRecord(ctx, edgegriddns.GetRecordRequest{ - Zone: zone, - Name: info.EffectiveFQDN, - RecordType: "TXT", - }) + record, err := configdns.GetRecord(zone, info.EffectiveFQDN, "TXT") if err != nil && !isNotFound(err) { return fmt.Errorf("edgedns: %w", err) } @@ -168,16 +135,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { record.Target = append(record.Target, `"`+info.Value+`"`) record.TTL = d.config.TTL - err = client.UpdateRecord(ctx, edgegriddns.UpdateRecordRequest{ - Record: &edgegriddns.RecordBody{ - Name: record.Name, - RecordType: record.RecordType, - TTL: record.TTL, - Active: record.Active, - Target: record.Target, - }, - Zone: zone, - }) + err = record.Update(zone) if err != nil { return fmt.Errorf("edgedns: %w", err) } @@ -185,16 +143,14 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return nil } - err = client.CreateRecord(ctx, edgegriddns.CreateRecordRequest{ - Record: &edgegriddns.RecordBody{ - Name: info.EffectiveFQDN, - RecordType: "TXT", - TTL: d.config.TTL, - Target: []string{`"` + info.Value + `"`}, - }, - Zone: zone, - RecLock: nil, - }) + record = &configdns.RecordBody{ + Name: info.EffectiveFQDN, + RecordType: "TXT", + TTL: d.config.TTL, + Target: []string{`"` + info.Value + `"`}, + } + + err = record.Save(zone) if err != nil { return fmt.Errorf("edgedns: %w", err) } @@ -204,32 +160,18 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) - sess, err := session.New(session.WithSigner(d.config)) - if err != nil { - return fmt.Errorf("edgedns: %w", err) - } - - client := edgegriddns.Client(sess) - zone, err := getZone(info.EffectiveFQDN) if err != nil { return fmt.Errorf("edgedns: %w", err) } - existingRec, err := client.GetRecord(ctx, edgegriddns.GetRecordRequest{ - Zone: zone, - Name: info.EffectiveFQDN, - RecordType: "TXT", - }) + existingRec, err := configdns.GetRecord(zone, info.EffectiveFQDN, "TXT") if err != nil { if isNotFound(err) { return nil } - return fmt.Errorf("edgedns: %w", err) } @@ -245,21 +187,19 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return nil } - newRData := filterRData(existingRec, info) + var newRData []string + for _, val := range existingRec.Target { + val = strings.Trim(val, `"`) + if val == info.Value { + continue + } + newRData = append(newRData, val) + } if len(newRData) > 0 { existingRec.Target = newRData - err = client.UpdateRecord(ctx, edgegriddns.UpdateRecordRequest{ - Record: &edgegriddns.RecordBody{ - Name: existingRec.Name, - RecordType: existingRec.RecordType, - TTL: existingRec.TTL, - Active: existingRec.Active, - Target: existingRec.Target, - }, - Zone: zone, - }) + err = existingRec.Update(zone) if err != nil { return fmt.Errorf("edgedns: %w", err) } @@ -267,12 +207,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return nil } - err = client.DeleteRecord(ctx, edgegriddns.DeleteRecordRequest{ - Zone: zone, - Name: existingRec.Name, - RecordType: "TXT", - RecLock: nil, - }) + err = existingRec.Delete(zone) if err != nil { return fmt.Errorf("edgedns: %w", err) } @@ -300,22 +235,6 @@ func isNotFound(err error) bool { return false } - var e *edgegriddns.Error - - return errors.As(err, &e) && e.StatusCode == http.StatusNotFound -} - -func filterRData(existingRec *edgegriddns.GetRecordResponse, info dns01.ChallengeInfo) []string { - var newRData []string - - for _, val := range existingRec.Target { - val = strings.Trim(val, `"`) - if val == info.Value { - continue - } - - newRData = append(newRData, val) - } - - return newRData + var e configdns.ConfigDNSError + return errors.As(err, &e) && e.NotFound() } diff --git a/providers/dns/edgedns/edgedns.toml b/providers/dns/edgedns/edgedns.toml index 7c7c5b3aa..dffa6e46c 100644 --- a/providers/dns/edgedns/edgedns.toml +++ b/providers/dns/edgedns/edgedns.toml @@ -4,7 +4,6 @@ Akamai edgedns supersedes FastDNS; implementing a DNS provider for solving the D ''' URL = "https://www.akamai.com/us/en/products/security/edge-dns.jsp" Code = "edgedns" -Aliases = ["fastdns"] # "fastdns" is for compatibility with v3, must be dropped in v5 Since = "v3.9.0" Example = ''' @@ -12,7 +11,7 @@ AKAMAI_CLIENT_SECRET=abcdefghijklmnopqrstuvwxyz1234567890ABCDEFG= \ AKAMAI_CLIENT_TOKEN=akab-mnbvcxzlkjhgfdsapoiuytrewq1234567 \ AKAMAI_HOST=akab-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.luna.akamaiapis.net \ AKAMAI_ACCESS_TOKEN=akab-1234567890qwerty-asdfghjklzxcvtnu \ -lego --dns edgedns -d '*.example.com' -d example.com run +lego --email you@example.com --dns edgedns --domains my.example.org run ''' Additional = ''' @@ -42,7 +41,6 @@ See also: - [.edgerc Format](https://developer.akamai.com/legacy/introduction/Conf_Client.html#edgercformat) - [API Client Authentication](https://developer.akamai.com/legacy/introduction/Client_Auth.html) - [Config from Env](https://github.com/akamai/AkamaiOPEN-edgegrid-golang/blob/master/pkg/edgegrid/config.go#L118) -- [Manage many accounts](https://techdocs.akamai.com/developer/docs/manage-many-accounts-with-one-api-client) ''' [Configuration] @@ -54,10 +52,9 @@ See also: AKAMAI_EDGERC = "Path to the .edgerc file, managed by the Akamai EdgeGrid client" AKAMAI_EDGERC_SECTION = "Configuration section, managed by the Akamai EdgeGrid client" [Configuration.Additional] - AKAMAI_ACCOUNT_SWITCH_KEY = "Target account ID when the DNS zone and credentials belong to different accounts" - AKAMAI_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 15)" - AKAMAI_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 180)" - AKAMAI_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + AKAMAI_POLLING_INTERVAL = "Time between DNS propagation check. Default: 15 seconds" + AKAMAI_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation. Default: 3 minutes" + AKAMAI_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://developer.akamai.com/api/cloud_security/edge_dns_zone_management/v2.html" diff --git a/providers/dns/edgedns/edgedns_integration_test.go b/providers/dns/edgedns/edgedns_integration_test.go index d20b8e5aa..e1b3bb7cf 100644 --- a/providers/dns/edgedns/edgedns_integration_test.go +++ b/providers/dns/edgedns/edgedns_integration_test.go @@ -1,13 +1,11 @@ package edgedns import ( - "context" "fmt" "testing" "time" - edgegriddns "github.com/akamai/AkamaiOPEN-edgegrid-golang/v11/pkg/dns" - "github.com/akamai/AkamaiOPEN-edgegrid-golang/v11/pkg/session" + configdns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -19,7 +17,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -37,7 +34,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -73,21 +69,10 @@ func TestLiveTTL(t *testing.T) { zone, err := getZone(fqdn) require.NoError(t, err) - ctx := context.Background() - - sess, err := session.New(session.WithSigner(provider.config)) + resourceRecordSets, err := configdns.GetRecordList(zone, fqdn, "TXT") require.NoError(t, err) - client := edgegriddns.Client(sess) - - resourceRecordSets, err := client.GetRecordList(ctx, edgegriddns.GetRecordListRequest{ - Zone: zone, - RecordType: "TXT", - }) - - require.NoError(t, err) - - for i, rrset := range resourceRecordSets.RecordSets { + for i, rrset := range resourceRecordSets.Recordsets { if rrset.Name != fqdn { continue } diff --git a/providers/dns/edgedns/edgedns_test.go b/providers/dns/edgedns/edgedns_test.go index a64efd6e2..9bb76580b 100644 --- a/providers/dns/edgedns/edgedns_test.go +++ b/providers/dns/edgedns/edgedns_test.go @@ -1,10 +1,12 @@ package edgedns import ( + "os" "testing" "time" - "github.com/akamai/AkamaiOPEN-edgegrid-golang/v11/pkg/edgegrid" + configdns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" @@ -19,14 +21,10 @@ const ( ) var envTest = tester.NewEnvTest( - EnvTTL, - EnvPollingInterval, - EnvPropagationTimeout, EnvHost, EnvClientToken, EnvClientSecret, EnvAccessToken, - EnvAccountSwitchKey, EnvEdgeRc, EnvEdgeRcSection, envTestHost, @@ -36,7 +34,7 @@ var envTest = tester.NewEnvTest( WithDomain(envDomain). WithLiveTestRequirements(EnvHost, EnvClientToken, EnvClientSecret, EnvAccessToken, envDomain) -func TestNewDNSProvider(t *testing.T) { +func TestNewDNSProvider_FromEnv(t *testing.T) { testCases := []struct { desc string envVars map[string]string @@ -51,31 +49,13 @@ func TestNewDNSProvider(t *testing.T) { EnvClientSecret: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", EnvAccessToken: "akac-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx", }, - expectedConfig: newEdgeConfig(func(config *edgegrid.Config) { - config.Host = "akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net" - config.ClientToken = "akab-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx" - config.ClientSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - config.AccessToken = "akac-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx" - config.MaxBody = maxBody - }, edgegrid.WithEnv(true), edgegrid.WithFile("/dev/null")), - }, - { - desc: "with account switch key", - envVars: map[string]string{ - EnvHost: "akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net", - EnvClientToken: "akab-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx", - EnvClientSecret: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", - EnvAccessToken: "akac-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx", - EnvAccountSwitchKey: "F-AC-1234", + expectedConfig: &edgegrid.Config{ + Host: "akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net", + ClientToken: "akab-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx", + ClientSecret: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + AccessToken: "akac-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx", + MaxBody: maxBody, }, - expectedConfig: newEdgeConfig(func(config *edgegrid.Config) { - config.Host = "akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net" - config.ClientToken = "akab-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx" - config.ClientSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - config.AccessToken = "akac-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx" - config.MaxBody = maxBody - config.AccountKey = "F-AC-1234" - }, edgegrid.WithEnv(true), edgegrid.WithFile("/dev/null")), }, { desc: "with section", @@ -86,17 +66,17 @@ func TestNewDNSProvider(t *testing.T) { envTestClientSecret: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", envTestAccessToken: "akac-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx", }, - expectedConfig: newEdgeConfig(func(config *edgegrid.Config) { - config.Host = "akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net" - config.ClientToken = "akab-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx" - config.ClientSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - config.AccessToken = "akac-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx" - config.MaxBody = maxBody - }, edgegrid.WithEnv(true), edgegrid.WithFile("/dev/null"), edgegrid.WithSection("test")), + expectedConfig: &edgegrid.Config{ + Host: "akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net", + ClientToken: "akab-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx", + ClientSecret: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + AccessToken: "akac-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx", + MaxBody: maxBody, + }, }, { desc: "missing credentials", - expectedErr: `edgedns: unable to load config from environment or .edgerc file`, + expectedErr: "edgedns: Unable to create instance using environment or .edgerc file", }, { desc: "missing host", @@ -106,7 +86,7 @@ func TestNewDNSProvider(t *testing.T) { EnvClientSecret: "C", EnvAccessToken: "D", }, - expectedErr: `edgedns: unable to load config from environment or .edgerc file`, + expectedErr: "edgedns: Unable to create instance using environment or .edgerc file", }, { desc: "missing client token", @@ -116,7 +96,7 @@ func TestNewDNSProvider(t *testing.T) { EnvClientSecret: "C", EnvAccessToken: "D", }, - expectedErr: `edgedns: unable to load config from environment or .edgerc file`, + expectedErr: "edgedns: Fatal missing required environment variables: [AKAMAI_CLIENT_TOKEN]", }, { desc: "missing client secret", @@ -126,7 +106,7 @@ func TestNewDNSProvider(t *testing.T) { EnvClientSecret: "", EnvAccessToken: "D", }, - expectedErr: `edgedns: unable to load config from environment or .edgerc file`, + expectedErr: "edgedns: Fatal missing required environment variables: [AKAMAI_CLIENT_SECRET]", }, { desc: "missing access token", @@ -136,20 +116,18 @@ func TestNewDNSProvider(t *testing.T) { EnvClientSecret: "C", EnvAccessToken: "", }, - expectedErr: `edgedns: unable to load config from environment or .edgerc file`, + expectedErr: "edgedns: Fatal missing required environment variables: [AKAMAI_ACCESS_TOKEN]", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() if test.envVars == nil { test.envVars = map[string]string{} } - test.envVars[EnvEdgeRc] = "/dev/null" envTest.Apply(test.envVars) @@ -157,7 +135,7 @@ func TestNewDNSProvider(t *testing.T) { p, err := NewDNSProvider() if test.expectedErr != "" { - require.ErrorContains(t, err, test.expectedErr) + require.EqualError(t, err, test.expectedErr) return } @@ -166,63 +144,13 @@ func TestNewDNSProvider(t *testing.T) { require.NotNil(t, p.config) if test.expectedConfig != nil { - require.Equal(t, test.expectedConfig, p.config.Config) + require.Equal(t, *test.expectedConfig, configdns.Config) } }) } } -func TestNewDefaultConfig(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected *Config - }{ - { - desc: "default configuration", - expected: &Config{ - TTL: dns01.DefaultTTL, - PropagationTimeout: 3 * time.Minute, - PollingInterval: 15 * time.Second, - Config: &edgegrid.Config{ - MaxBody: maxBody, - }, - }, - }, - { - desc: "custom values", - envVars: map[string]string{ - EnvTTL: "99", - EnvPropagationTimeout: "60", - EnvPollingInterval: "60", - }, - expected: &Config{ - TTL: 99, - PropagationTimeout: 60 * time.Second, - PollingInterval: 60 * time.Second, - Config: &edgegrid.Config{ - MaxBody: maxBody, - }, - }, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - config := NewDefaultConfig() - - require.Equal(t, test.expected, config) - }) - } -} - -func Test_findZone(t *testing.T) { +func TestDNSProvider_findZone(t *testing.T) { testCases := []struct { desc string domain string @@ -251,7 +179,53 @@ func Test_findZone(t *testing.T) { } } -func newEdgeConfig(opts ...edgegrid.Option) *edgegrid.Config { - config, _ := edgegrid.New(opts...) - return config +func TestNewDefaultConfig(t *testing.T) { + defer envTest.RestoreEnv() + + testCases := []struct { + desc string + envVars map[string]string + expected *Config + }{ + { + desc: "default configuration", + expected: &Config{ + TTL: dns01.DefaultTTL, + PropagationTimeout: 3 * time.Minute, + PollingInterval: 15 * time.Second, + Config: edgegrid.Config{ + MaxBody: maxBody, + }, + }, + }, + { + desc: "custom values", + envVars: map[string]string{ + EnvTTL: "99", + EnvPropagationTimeout: "60", + EnvPollingInterval: "60", + }, + expected: &Config{ + TTL: 99, + PropagationTimeout: 60 * time.Second, + PollingInterval: 60 * time.Second, + Config: edgegrid.Config{ + MaxBody: maxBody, + }, + }, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + envTest.ClearEnv() + for key, value := range test.envVars { + os.Setenv(key, value) + } + + config := NewDefaultConfig() + + require.Equal(t, test.expected, config) + }) + } } diff --git a/providers/dns/edgeone/edgeone.go b/providers/dns/edgeone/edgeone.go deleted file mode 100644 index 6931c6715..000000000 --- a/providers/dns/edgeone/edgeone.go +++ /dev/null @@ -1,203 +0,0 @@ -// Package edgeone implements a DNS provider for solving the DNS-01 challenge using Tencent EdgeOne. -package edgeone - -import ( - "context" - "errors" - "fmt" - "math" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/ptr" - teo "github.com/go-acme/tencentedgdeone/v20220901" - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" - "golang.org/x/net/idna" -) - -// Environment variables names. -const ( - envNamespace = "EDGEONE_" - - EnvSecretID = envNamespace + "SECRET_ID" - EnvSecretKey = envNamespace + "SECRET_KEY" - EnvRegion = envNamespace + "REGION" - EnvSessionToken = envNamespace + "SESSION_TOKEN" - EnvZonesMapping = envNamespace + "ZONES_MAPPING" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - SecretID string - SecretKey string - Region string - SessionToken string - - ZonesMapping map[string]string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPTimeout time.Duration -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, 60), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 20*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 30*time.Second), - HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *teo.Client - - recordIDs map[string]*string - recordIDsMu sync.Mutex -} - -// NewDNSProvider returns a DNSProvider instance configured for Tencent EdgeOne. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvSecretID, EnvSecretKey) - if err != nil { - return nil, fmt.Errorf("edgeone: %w", err) - } - - config := NewDefaultConfig() - config.SecretID = values[EnvSecretID] - config.SecretKey = values[EnvSecretKey] - config.Region = env.GetOrDefaultString(EnvRegion, "") - config.SessionToken = env.GetOrDefaultString(EnvSessionToken, "") - - mapping := env.GetOrDefaultString(EnvZonesMapping, "") - if mapping != "" { - config.ZonesMapping, err = env.ParsePairs(mapping) - if err != nil { - return nil, fmt.Errorf("edgeone: zones mapping: %w", err) - } - } - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Tencent EdgeOne. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("edgeone: the configuration of the DNS provider is nil") - } - - var credential *common.Credential - - switch { - case config.SecretID != "" && config.SecretKey != "" && config.SessionToken != "": - credential = common.NewTokenCredential(config.SecretID, config.SecretKey, config.SessionToken) - case config.SecretID != "" && config.SecretKey != "": - credential = common.NewCredential(config.SecretID, config.SecretKey) - default: - return nil, errors.New("edgeone: credentials missing") - } - - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "teo.intl.tencentcloudapi.com" - cpf.HttpProfile.ReqTimeout = int(math.Round(config.HTTPTimeout.Seconds())) - - client, err := teo.NewClient(credential, config.Region, cpf) - if err != nil { - return nil, fmt.Errorf("edgeone: %w", err) - } - - return &DNSProvider{ - config: config, - client: client, - recordIDs: map[string]*string{}, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx := context.Background() - - zoneID, err := d.getHostedZoneID(ctx, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("edgeone: failed to get hosted zone: %w", err) - } - - punnyCoded, err := idna.ToASCII(dns01.UnFqdn(info.EffectiveFQDN)) - if err != nil { - return fmt.Errorf("edgeone: fail to convert punycode: %w", err) - } - - request := teo.NewCreateDnsRecordRequest() - request.Name = ptr.Pointer(punnyCoded) - request.ZoneId = zoneID - request.Type = ptr.Pointer("TXT") - request.Content = ptr.Pointer(info.Value) - request.TTL = ptr.Pointer(int64(d.config.TTL)) - - nr, err := teo.CreateDnsRecordWithContext(ctx, d.client, request) - if err != nil { - return fmt.Errorf("edgeone: API call failed: %w", err) - } - - d.recordIDsMu.Lock() - d.recordIDs[token] = nr.Response.RecordId - d.recordIDsMu.Unlock() - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx := context.Background() - - zoneID, err := d.getHostedZoneID(ctx, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("edgeone: failed to get hosted zone: %w", err) - } - - // get the record's unique ID from when we created it - d.recordIDsMu.Lock() - recordID, ok := d.recordIDs[token] - d.recordIDsMu.Unlock() - - if !ok { - return fmt.Errorf("edgeone: unknown record ID for '%s'", info.EffectiveFQDN) - } - - request := teo.NewDeleteDnsRecordsRequest() - request.ZoneId = zoneID - request.RecordIds = []*string{recordID} - - _, err = teo.DeleteDnsRecordsWithContext(ctx, d.client, request) - if err != nil { - return fmt.Errorf("edgeone: delete record failed: %w", err) - } - - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/edgeone/edgeone.toml b/providers/dns/edgeone/edgeone.toml deleted file mode 100644 index 05b8bc516..000000000 --- a/providers/dns/edgeone/edgeone.toml +++ /dev/null @@ -1,28 +0,0 @@ -Name = "Tencent EdgeOne" -Description = '''''' -URL = "https://edgeone.ai" -Code = "edgeone" -Since = "v4.26.0" - -Example = ''' -EDGEONE_SECRET_ID=abcdefghijklmnopqrstuvwx \ -EDGEONE_SECRET_KEY=your-secret-key \ -lego --dns edgeone -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - EDGEONE_SECRET_ID = "Access key ID" - EDGEONE_SECRET_KEY = "Access Key secret" - [Configuration.Additional] - EDGEONE_SESSION_TOKEN = "Access Key token" - EDGEONE_REGION = "Region" - EDGEONE_ZONES_MAPPING = "Mapping between DNS zones and site IDs. (ex: 'example.org:id1,example.com:id2')" - EDGEONE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 30)" - EDGEONE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 1200)" - EDGEONE_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" - EDGEONE_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://edgeone.ai/document/50454#dns-record-apis" - GoClient = "https://github.com/tencentcloud/tencentcloud-sdk-go" diff --git a/providers/dns/edgeone/edgeone_test.go b/providers/dns/edgeone/edgeone_test.go deleted file mode 100644 index 7bd4f6f6d..000000000 --- a/providers/dns/edgeone/edgeone_test.go +++ /dev/null @@ -1,170 +0,0 @@ -package edgeone - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest( - EnvSecretID, - EnvSecretKey, - EnvZonesMapping, -).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvSecretID: "123", - EnvSecretKey: "456", - }, - }, - { - desc: "success with zones mapping", - envVars: map[string]string{ - EnvSecretID: "123", - EnvSecretKey: "456", - EnvZonesMapping: "example.org:id1,example.com:id2", - }, - }, - { - desc: "missing credentials", - envVars: map[string]string{ - EnvSecretID: "", - EnvSecretKey: "", - }, - expected: "edgeone: some credentials information are missing: EDGEONE_SECRET_ID,EDGEONE_SECRET_KEY", - }, - { - desc: "missing access id", - envVars: map[string]string{ - EnvSecretID: "", - EnvSecretKey: "456", - }, - expected: "edgeone: some credentials information are missing: EDGEONE_SECRET_ID", - }, - { - desc: "missing secret key", - envVars: map[string]string{ - EnvSecretID: "123", - EnvSecretKey: "", - }, - expected: "edgeone: some credentials information are missing: EDGEONE_SECRET_KEY", - }, - { - desc: "invalid mapping", - envVars: map[string]string{ - EnvSecretID: "123", - EnvSecretKey: "456", - EnvZonesMapping: "example.org:id1,example.com", - }, - expected: "edgeone: zones mapping: incorrect pair: example.com", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - secretID string - secretKey string - expected string - }{ - { - desc: "success", - secretID: "123", - secretKey: "456", - }, - { - desc: "missing credentials", - expected: "edgeone: credentials missing", - }, - { - desc: "missing secret id", - secretKey: "456", - expected: "edgeone: credentials missing", - }, - { - desc: "missing secret key", - secretID: "123", - expected: "edgeone: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.SecretID = test.secretID - config.SecretKey = test.secretKey - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/edgeone/wrapper.go b/providers/dns/edgeone/wrapper.go deleted file mode 100644 index 53fae9427..000000000 --- a/providers/dns/edgeone/wrapper.go +++ /dev/null @@ -1,58 +0,0 @@ -package edgeone - -import ( - "context" - "fmt" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/providers/dns/internal/ptr" - teo "github.com/go-acme/tencentedgdeone/v20220901" -) - -func (d *DNSProvider) getHostedZoneID(ctx context.Context, domain string) (*string, error) { - authZone, err := dns01.FindZoneByFqdn(domain) - if err != nil { - return nil, fmt.Errorf("could not find zone: %w", err) - } - - if d.config.ZonesMapping != nil { - zoneID, ok := d.config.ZonesMapping[authZone] - if ok { - return ptr.Pointer(zoneID), nil - } - } - - request := teo.NewDescribeZonesRequest() - - var zones []*teo.Zone - - for { - response, err := teo.DescribeZonesWithContext(ctx, d.client, request) - if err != nil { - return nil, fmt.Errorf("API call failed: %w", err) - } - - zones = append(zones, response.Response.Zones...) - - if int64(len(zones)) >= ptr.Deref(response.Response.TotalCount) { - break - } - - request.Offset = ptr.Pointer(int64(len(zones))) - } - - var hostedZone *teo.Zone - - for _, zone := range zones { - unfqdn := dns01.UnFqdn(authZone) - if ptr.Deref(zone.ZoneName) == unfqdn { - hostedZone = zone - } - } - - if hostedZone == nil { - return nil, fmt.Errorf("zone %s not found for domain %s", authZone, domain) - } - - return hostedZone.ZoneId, nil -} diff --git a/providers/dns/efficientip/efficientip.go b/providers/dns/efficientip/efficientip.go index 81b4530b7..6d639bce1 100644 --- a/providers/dns/efficientip/efficientip.go +++ b/providers/dns/efficientip/efficientip.go @@ -9,11 +9,9 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/efficientip/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -32,8 +30,6 @@ const ( EnvInsecureSkipVerify = envNamespace + "INSECURE_SKIP_VERIFY" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Username string @@ -92,15 +88,12 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config.Username == "" { return nil, errors.New("efficientip: missing username") } - if config.Password == "" { return nil, errors.New("efficientip: missing password") } - if config.Hostname == "" { return nil, errors.New("efficientip: missing hostname") } - if config.DNSName == "" { return nil, errors.New("efficientip: missing dnsname") } @@ -117,8 +110,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { } } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{config: config, client: client}, nil } diff --git a/providers/dns/efficientip/efficientip.toml b/providers/dns/efficientip/efficientip.toml index 6e1874319..cd2022807 100644 --- a/providers/dns/efficientip/efficientip.toml +++ b/providers/dns/efficientip/efficientip.toml @@ -9,7 +9,7 @@ EFFICIENTIP_USERNAME="user" \ EFFICIENTIP_PASSWORD="secret" \ EFFICIENTIP_HOSTNAME="ipam.example.org" \ EFFICIENTIP_DNS_NAME="dns.smart" \ -lego --dns efficientip -d '*.example.com' -d example.com run +lego --email you@example.com --dns efficientip --domains my.example.org run ''' [Configuration] @@ -21,6 +21,7 @@ lego --dns efficientip -d '*.example.com' -d example.com run [Configuration.Additional] EFFICIENTIP_INSECURE_SKIP_VERIFY = "Whether or not to verify EfficientIP API certificate" EFFICIENTIP_VIEW_NAME = "View name (ex: external)" - EFFICIENTIP_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - EFFICIENTIP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - EFFICIENTIP_HTTP_TIMEOUT = "API request timeout in seconds (Default: 10)" + EFFICIENTIP_POLLING_INTERVAL = "Time between DNS propagation check" + EFFICIENTIP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + EFFICIENTIP_TTL = "The TTL of the TXT record used for the DNS challenge" + EFFICIENTIP_HTTP_TIMEOUT = "API request timeout" diff --git a/providers/dns/efficientip/efficientip_test.go b/providers/dns/efficientip/efficientip_test.go index c2751a79b..3ee2da777 100644 --- a/providers/dns/efficientip/efficientip_test.go +++ b/providers/dns/efficientip/efficientip_test.go @@ -83,7 +83,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -179,7 +178,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -193,7 +191,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/efficientip/internal/client.go b/providers/dns/efficientip/internal/client.go index 5ccdf3973..2fea76a13 100644 --- a/providers/dns/efficientip/internal/client.go +++ b/providers/dns/efficientip/internal/client.go @@ -22,7 +22,7 @@ type Client struct { password string } -func NewClient(hostname, username, password string) *Client { +func NewClient(hostname string, username string, password string) *Client { baseURL, _ := url.Parse(fmt.Sprintf("https://%s/rest/", hostname)) return &Client{ @@ -33,7 +33,7 @@ func NewClient(hostname, username, password string) *Client { } } -func (c *Client) ListRecords(ctx context.Context) ([]ResourceRecord, error) { +func (c Client) ListRecords(ctx context.Context) ([]ResourceRecord, error) { endpoint := c.baseURL.JoinPath("dns_rr_list") req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) @@ -51,7 +51,7 @@ func (c *Client) ListRecords(ctx context.Context) ([]ResourceRecord, error) { return result, nil } -func (c *Client) GetRecord(ctx context.Context, id string) (*ResourceRecord, error) { +func (c Client) GetRecord(ctx context.Context, id string) (*ResourceRecord, error) { endpoint := c.baseURL.JoinPath("dns_rr_info") query := endpoint.Query() @@ -77,7 +77,7 @@ func (c *Client) GetRecord(ctx context.Context, id string) (*ResourceRecord, err return &result[0], nil } -func (c *Client) AddRecord(ctx context.Context, record ResourceRecord) (*BaseOutput, error) { +func (c Client) AddRecord(ctx context.Context, record ResourceRecord) (*BaseOutput, error) { endpoint := c.baseURL.JoinPath("dns_rr_add") req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) @@ -99,7 +99,7 @@ func (c *Client) AddRecord(ctx context.Context, record ResourceRecord) (*BaseOut return &result[0], nil } -func (c *Client) DeleteRecord(ctx context.Context, params DeleteInputParameters) (*BaseOutput, error) { +func (c Client) DeleteRecord(ctx context.Context, params DeleteInputParameters) (*BaseOutput, error) { endpoint := c.baseURL.JoinPath("dns_rr_delete") // (rr_id || (rr_name && (dns_id || dns_name || hostaddr))) @@ -108,7 +108,6 @@ func (c *Client) DeleteRecord(ctx context.Context, params DeleteInputParameters) if err != nil { return nil, fmt.Errorf("query parameters: %w", err) } - endpoint.RawQuery = v.Encode() req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) @@ -130,7 +129,7 @@ func (c *Client) DeleteRecord(ctx context.Context, params DeleteInputParameters) return &result[0], nil } -func (c *Client) do(req *http.Request, result any) error { +func (c Client) do(req *http.Request, result any) error { req.SetBasicAuth(c.username, c.password) req.Header.Set("cache-control", "no-cache") @@ -201,7 +200,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) var response APIError - err := json.Unmarshal(raw, &response) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/efficientip/internal/client_test.go b/providers/dns/efficientip/internal/client_test.go index 5d68b7d7f..a766c9085 100644 --- a/providers/dns/efficientip/internal/client_test.go +++ b/providers/dns/efficientip/internal/client_test.go @@ -1,38 +1,80 @@ package internal import ( + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - srvURL, _ := url.Parse(server.URL) +func setupTest(t *testing.T, method, pattern string, status int, file string) *Client { + t.Helper() - client := NewClient(srvURL.Host, "user", "secret") - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithBasicAuth("user", "secret"), - ) + mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + username, password, ok := req.BasicAuth() + if !ok { + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + if username != "user" { + http.Error(rw, fmt.Sprintf("username: want %s got %s", username, "user"), http.StatusUnauthorized) + return + } + + if password != "secret" { + http.Error(rw, fmt.Sprintf("password: want %s got %s", password, "secret"), http.StatusUnauthorized) + return + } + + open, err := os.Open(filepath.Join("fixtures", file)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = open.Close() }() + + rw.WriteHeader(status) + _, err = io.Copy(rw, open) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + srvURL, _ := url.Parse(server.URL) + + client := NewClient(srvURL.Host, "user", "secret") + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client } func TestListRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /dns_rr_list", servermock.ResponseFromFixture("dns_rr_list.json")). - Build(t) + client := setupTest(t, http.MethodGet, "/dns_rr_list", http.StatusOK, "dns_rr_list.json") - records, err := client.ListRecords(t.Context()) + ctx := context.Background() + + records, err := client.ListRecords(ctx) require.NoError(t, err) expected := []ResourceRecord{ @@ -295,13 +337,11 @@ func TestListRecords(t *testing.T) { } func TestGetRecord(t *testing.T) { - client := mockBuilder(). - Route("GET /dns_rr_info", servermock.ResponseFromFixture("dns_rr_info.json"), - servermock.CheckQueryParameter().Strict(). - With("rr_id", "239")). - Build(t) + client := setupTest(t, http.MethodGet, "/dns_rr_info", http.StatusOK, "dns_rr_info.json") - record, err := client.GetRecord(t.Context(), "239") + ctx := context.Background() + + record, err := client.GetRecord(ctx, "239") require.NoError(t, err) expected := &ResourceRecord{ @@ -344,11 +384,9 @@ func TestGetRecord(t *testing.T) { } func TestAddRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /dns_rr_add", - servermock.ResponseFromFixture("dns_rr_add.json").WithStatusCode(http.StatusCreated), - servermock.CheckRequestJSONBody(`{"dns_name":"dns.smart","dnsview_name":"external","rr_name":"test.example.com","rr_type":"TXT","value1":"test"}`)). - Build(t) + client := setupTest(t, http.MethodPost, "/dns_rr_add", http.StatusCreated, "dns_rr_add.json") + + ctx := context.Background() r := ResourceRecord{ RRName: "test.example.com", @@ -358,7 +396,7 @@ func TestAddRecord(t *testing.T) { DNSViewName: "external", } - resp, err := client.AddRecord(t.Context(), r) + resp, err := client.AddRecord(ctx, r) require.NoError(t, err) expected := &BaseOutput{RetOID: "239"} @@ -367,13 +405,11 @@ func TestAddRecord(t *testing.T) { } func TestDeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /dns_rr_delete", servermock.ResponseFromFixture("dns_rr_delete.json"), - servermock.CheckQueryParameter().Strict(). - With("rr_id", "251")). - Build(t) + client := setupTest(t, http.MethodDelete, "/dns_rr_delete", http.StatusOK, "dns_rr_delete.json") - resp, err := client.DeleteRecord(t.Context(), DeleteInputParameters{RRID: "251"}) + ctx := context.Background() + + resp, err := client.DeleteRecord(ctx, DeleteInputParameters{RRID: "251"}) require.NoError(t, err) expected := &BaseOutput{RetOID: "251"} @@ -382,11 +418,10 @@ func TestDeleteRecord(t *testing.T) { } func TestDeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /dns_rr_delete", - servermock.ResponseFromFixture("dns_rr_delete-error.json").WithStatusCode(http.StatusBadRequest)). - Build(t) + client := setupTest(t, http.MethodDelete, "/dns_rr_delete", http.StatusBadRequest, "dns_rr_delete-error.json") - _, err := client.DeleteRecord(t.Context(), DeleteInputParameters{RRID: "251"}) + ctx := context.Background() + + _, err := client.DeleteRecord(ctx, DeleteInputParameters{RRID: "251"}) require.ErrorAs(t, err, &APIError{}) } diff --git a/providers/dns/epik/epik.go b/providers/dns/epik/epik.go index ef5de3c4b..4d4fb8c73 100644 --- a/providers/dns/epik/epik.go +++ b/providers/dns/epik/epik.go @@ -9,11 +9,9 @@ import ( "strings" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/epik/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -28,8 +26,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Signature string @@ -87,8 +83,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{config: config, client: client}, nil } diff --git a/providers/dns/epik/epik.toml b/providers/dns/epik/epik.toml index faf453581..555d51c6b 100644 --- a/providers/dns/epik/epik.toml +++ b/providers/dns/epik/epik.toml @@ -6,17 +6,17 @@ Since = "v4.5.0" Example = ''' EPIK_SIGNATURE=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --dns epik -d '*.example.com' -d example.com run +lego --email you@example.com --dns epik --domains my.example.org run ''' [Configuration] [Configuration.Credentials] EPIK_SIGNATURE = "Epik API signature (https://registrar.epik.com/account/api-settings/)" [Configuration.Additional] - EPIK_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - EPIK_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - EPIK_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)" - EPIK_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + EPIK_POLLING_INTERVAL = "Time between DNS propagation check" + EPIK_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + EPIK_TTL = "The TTL of the TXT record used for the DNS challenge" + EPIK_HTTP_TIMEOUT = "API request timeout" [Links] - API = "https://docs-userapi.epik.com/v2/" + API = "https://docs.userapi.epik.com/v2/#/" diff --git a/providers/dns/epik/epik_test.go b/providers/dns/epik/epik_test.go index b8b3c5c43..c0cd3d43b 100644 --- a/providers/dns/epik/epik_test.go +++ b/providers/dns/epik/epik_test.go @@ -33,7 +33,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -93,7 +92,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -107,7 +105,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/epik/internal/client.go b/providers/dns/epik/internal/client.go index 2c3373953..0ca46c2cb 100644 --- a/providers/dns/epik/internal/client.go +++ b/providers/dns/epik/internal/client.go @@ -11,7 +11,6 @@ import ( "time" "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" ) const defaultBaseURL = "https://usersapiv2.epik.com/v2" @@ -37,7 +36,7 @@ func NewClient(signature string) *Client { // GetDNSRecords gets DNS records for a domain. // https://docs.userapi.epik.com/v2/#/DNS%20Host%20Records/getDnsRecord -func (c *Client) GetDNSRecords(ctx context.Context, domain string) ([]Record, error) { +func (c Client) GetDNSRecords(ctx context.Context, domain string) ([]Record, error) { endpoint := c.createEndpoint(domain, url.Values{}) req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) @@ -46,7 +45,6 @@ func (c *Client) GetDNSRecords(ctx context.Context, domain string) ([]Record, er } var data GetDNSRecordResponse - err = c.do(req, &data) if err != nil { return nil, err @@ -57,7 +55,7 @@ func (c *Client) GetDNSRecords(ctx context.Context, domain string) ([]Record, er // CreateHostRecord creates a record for a domain. // https://docs.userapi.epik.com/v2/#/DNS%20Host%20Records/createHostRecord -func (c *Client) CreateHostRecord(ctx context.Context, domain string, record RecordRequest) (*Data, error) { +func (c Client) CreateHostRecord(ctx context.Context, domain string, record RecordRequest) (*Data, error) { endpoint := c.createEndpoint(domain, url.Values{}) payload := CreateHostRecords{Payload: record} @@ -68,7 +66,6 @@ func (c *Client) CreateHostRecord(ctx context.Context, domain string, record Rec } var data Data - err = c.do(req, &data) if err != nil { return nil, err @@ -79,7 +76,7 @@ func (c *Client) CreateHostRecord(ctx context.Context, domain string, record Rec // RemoveHostRecord removes a record for a domain. // https://docs.userapi.epik.com/v2/#/DNS%20Host%20Records/removeHostRecord -func (c *Client) RemoveHostRecord(ctx context.Context, domain, recordID string) (*Data, error) { +func (c Client) RemoveHostRecord(ctx context.Context, domain string, recordID string) (*Data, error) { params := url.Values{} params.Set("ID", recordID) @@ -91,7 +88,6 @@ func (c *Client) RemoveHostRecord(ctx context.Context, domain, recordID string) } var data Data - err = c.do(req, &data) if err != nil { return nil, err @@ -100,9 +96,7 @@ func (c *Client) RemoveHostRecord(ctx context.Context, domain, recordID string) return &data, nil } -func (c *Client) do(req *http.Request, result any) error { - useragent.SetHeader(req.Header) - +func (c Client) do(req *http.Request, result any) error { resp, err := c.HTTPClient.Do(req) if err != nil { return errutils.NewHTTPDoError(req, err) @@ -131,7 +125,7 @@ func (c *Client) do(req *http.Request, result any) error { return nil } -func (c *Client) createEndpoint(domain string, params url.Values) *url.URL { +func (c Client) createEndpoint(domain string, params url.Values) *url.URL { endpoint := c.baseURL.JoinPath("domains", domain, "records") params.Set("SIGNATURE", c.signature) @@ -168,7 +162,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) var apiErr APIError - err := json.Unmarshal(raw, &apiErr) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/epik/internal/client_test.go b/providers/dns/epik/internal/client_test.go index b7c6f97df..78c4452f0 100644 --- a/providers/dns/epik/internal/client_test.go +++ b/providers/dns/epik/internal/client_test.go @@ -1,38 +1,40 @@ package internal import ( + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("secret") - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(), - ) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient("secret") + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client, mux } func TestClient_GetDNSRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /domains/example.com/records", - servermock.ResponseFromFixture("getDnsRecord.json"), - servermock.CheckQueryParameter().Strict(). - With("SIGNATURE", "secret")). - Build(t) + client, mux := setupTest(t) - records, err := client.GetDNSRecords(t.Context(), "example.com") + mux.HandleFunc("/domains/example.com/records", testHandler(http.MethodGet, http.StatusOK, "getDnsRecord.json")) + + records, err := client.GetDNSRecords(context.Background(), "example.com") require.NoError(t, err) expected := []Record{ @@ -87,25 +89,18 @@ func TestClient_GetDNSRecords(t *testing.T) { } func TestClient_GetDNSRecords_error(t *testing.T) { - client := mockBuilder(). - Route("GET /domains/example.com/records", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized), - servermock.CheckQueryParameter().Strict(). - With("SIGNATURE", "secret")). - Build(t) + client, mux := setupTest(t) - _, err := client.GetDNSRecords(t.Context(), "example.com") + mux.HandleFunc("/domains/example.com/records", testHandler(http.MethodGet, http.StatusUnauthorized, "error.json")) + + _, err := client.GetDNSRecords(context.Background(), "example.com") require.Error(t, err) } func TestClient_CreateHostRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/example.com/records", - servermock.ResponseFromFixture("createHostRecord.json"), - servermock.CheckQueryParameter().Strict(). - With("SIGNATURE", "secret")). - Build(t) + client, mux := setupTest(t) + + mux.HandleFunc("/domains/example.com/records", testHandler(http.MethodPost, http.StatusOK, "createHostRecord.json")) record := RecordRequest{ Host: "www2", @@ -115,7 +110,7 @@ func TestClient_CreateHostRecord(t *testing.T) { TTL: 300, } - data, err := client.CreateHostRecord(t.Context(), "example.com", record) + data, err := client.CreateHostRecord(context.Background(), "example.com", record) require.NoError(t, err) expected := &Data{ @@ -127,13 +122,9 @@ func TestClient_CreateHostRecord(t *testing.T) { } func TestClient_CreateHostRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/example.com/records", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized), - servermock.CheckQueryParameter().Strict(). - With("SIGNATURE", "secret")). - Build(t) + client, mux := setupTest(t) + + mux.HandleFunc("/domains/example.com/records", testHandler(http.MethodPost, http.StatusUnauthorized, "error.json")) record := RecordRequest{ Host: "www2", @@ -143,20 +134,16 @@ func TestClient_CreateHostRecord_error(t *testing.T) { TTL: 300, } - _, err := client.CreateHostRecord(t.Context(), "example.com", record) + _, err := client.CreateHostRecord(context.Background(), "example.com", record) require.Error(t, err) } func TestClient_RemoveHostRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /domains/example.com/records", - servermock.ResponseFromFixture("removeHostRecord.json"), - servermock.CheckQueryParameter().Strict(). - With("ID", "abc123"). - With("SIGNATURE", "secret")). - Build(t) + client, mux := setupTest(t) - data, err := client.RemoveHostRecord(t.Context(), "example.com", "abc123") + mux.HandleFunc("/domains/example.com/records", testHandler(http.MethodDelete, http.StatusOK, "removeHostRecord.json")) + + data, err := client.RemoveHostRecord(context.Background(), "example.com", "abc123") require.NoError(t, err) expected := &Data{ @@ -168,12 +155,45 @@ func TestClient_RemoveHostRecord(t *testing.T) { } func TestClient_RemoveHostRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /domains/example.com/records", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client, mux := setupTest(t) - _, err := client.RemoveHostRecord(t.Context(), "example.com", "abc123") + mux.HandleFunc("/domains/example.com/records", testHandler(http.MethodDelete, http.StatusUnauthorized, "error.json")) + + _, err := client.RemoveHostRecord(context.Background(), "example.com", "abc123") require.Error(t, err) } + +func testHandler(method string, statusCode int, filename string) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed) + return + } + + auth := req.URL.Query().Get("SIGNATURE") + if auth != "secret" { + http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized) + return + } + + rw.WriteHeader(statusCode) + + if statusCode == http.StatusNoContent { + return + } + + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError) + return + } + + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError) + return + } + } +} diff --git a/providers/dns/eurodns/eurodns.go b/providers/dns/eurodns/eurodns.go deleted file mode 100644 index 21ff3c3a9..000000000 --- a/providers/dns/eurodns/eurodns.go +++ /dev/null @@ -1,197 +0,0 @@ -// Package eurodns implements a DNS provider for solving the DNS-01 challenge using EuroDNS. -package eurodns - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/eurodns/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "EURODNS_" - - EnvApplicationID = envNamespace + "APP_ID" - EnvAPIKey = envNamespace + "API_KEY" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - ApplicationID string - APIKey string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, internal.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for EuroDNS. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvApplicationID, EnvAPIKey) - if err != nil { - return nil, fmt.Errorf("eurodns: %w", err) - } - - config := NewDefaultConfig() - config.ApplicationID = values[EnvApplicationID] - config.APIKey = values[EnvAPIKey] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for EuroDNS. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("eurodns: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.ApplicationID, config.APIKey) - if err != nil { - return nil, fmt.Errorf("eurodns: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("eurodns: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("eurodns: %w", err) - } - - authZone = dns01.UnFqdn(authZone) - - zone, err := d.client.GetZone(ctx, authZone) - if err != nil { - return fmt.Errorf("eurodns: get zone: %w", err) - } - - zone.Records = append(zone.Records, internal.Record{ - Type: "TXT", - Host: subDomain, - TTL: internal.TTLRounder(d.config.TTL), - RData: info.Value, - }) - - validation, err := d.client.ValidateZone(ctx, authZone, zone) - if err != nil { - return fmt.Errorf("eurodns: validate zone: %w", err) - } - - if validation.Report != nil && !validation.Report.IsValid { - return fmt.Errorf("eurodns: validation report: %w", validation.Report) - } - - err = d.client.SaveZone(ctx, authZone, zone) - if err != nil { - return fmt.Errorf("eurodns: save zone: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("eurodns: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("eurodns: %w", err) - } - - authZone = dns01.UnFqdn(authZone) - - zone, err := d.client.GetZone(ctx, authZone) - if err != nil { - return fmt.Errorf("eurodns: get zone: %w", err) - } - - var recordsToKeep []internal.Record - - for _, record := range zone.Records { - if record.Type == "TXT" && record.Host == subDomain && record.RData == info.Value { - continue - } - - recordsToKeep = append(recordsToKeep, record) - } - - zone.Records = recordsToKeep - - validation, err := d.client.ValidateZone(ctx, authZone, zone) - if err != nil { - return fmt.Errorf("eurodns: validate zone: %w", err) - } - - if validation.Report != nil && !validation.Report.IsValid { - return fmt.Errorf("eurodns: validation report: %w", validation.Report) - } - - err = d.client.SaveZone(ctx, authZone, zone) - if err != nil { - return fmt.Errorf("eurodns: save zone: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/eurodns/eurodns.toml b/providers/dns/eurodns/eurodns.toml deleted file mode 100644 index 302b15d00..000000000 --- a/providers/dns/eurodns/eurodns.toml +++ /dev/null @@ -1,24 +0,0 @@ -Name = "EuroDNS" -Description = '''''' -URL = "https://www.eurodns.com/" -Code = "eurodns" -Since = "v4.33.0" - -Example = ''' -EURODNS_APP_ID="xxx" \ -EURODNS_API_KEY="yyy" \ -lego --dns eurodns -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - EURODNS_APP_ID = "Application ID" - EURODNS_API_KEY = "API key" - [Configuration.Additional] - EURODNS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - EURODNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - EURODNS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)" - EURODNS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://docapi.eurodns.com/" diff --git a/providers/dns/eurodns/eurodns_test.go b/providers/dns/eurodns/eurodns_test.go deleted file mode 100644 index abbb4717e..000000000 --- a/providers/dns/eurodns/eurodns_test.go +++ /dev/null @@ -1,215 +0,0 @@ -package eurodns - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/go-acme/lego/v4/providers/dns/eurodns/internal" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvApplicationID, EnvAPIKey).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvApplicationID: "abc", - EnvAPIKey: "secret", - }, - }, - { - desc: "missing application ID", - envVars: map[string]string{ - EnvApplicationID: "", - EnvAPIKey: "secret", - }, - expected: "eurodns: some credentials information are missing: EURODNS_APP_ID", - }, - { - desc: "missing API secret", - envVars: map[string]string{ - EnvApplicationID: "", - EnvAPIKey: "secret", - }, - expected: "eurodns: some credentials information are missing: EURODNS_APP_ID", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "eurodns: some credentials information are missing: EURODNS_APP_ID,EURODNS_API_KEY", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - appID string - apiKey string - expected string - }{ - { - desc: "success", - appID: "abc", - apiKey: "secret", - }, - { - desc: "missing application ID", - expected: "eurodns: credentials missing", - apiKey: "secret", - }, - { - desc: "missing API secret", - expected: "eurodns: credentials missing", - appID: "abc", - }, - { - desc: "missing credentials", - expected: "eurodns: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.ApplicationID = test.appID - config.APIKey = test.apiKey - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIKey = "secret" - config.ApplicationID = "abc" - config.HTTPClient = server.Client() - - provider, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - provider.client.BaseURL, _ = url.Parse(server.URL) - - return provider, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(). - With(internal.HeaderAppID, "abc"). - With(internal.HeaderAPIKey, "secret"), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("GET /example.com", - servermock.ResponseFromInternal("zone_get.json"), - ). - Route("POST /example.com/check", - servermock.ResponseFromInternal("zone_add_validate_ok.json"), - servermock.CheckRequestJSONBodyFromInternal("zone_add.json"), - ). - Route("PUT /example.com", - servermock.Noop(). - WithStatusCode(http.StatusNoContent), - servermock.CheckRequestJSONBodyFromInternal("zone_add.json"), - ). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("GET /example.com", - servermock.ResponseFromInternal("zone_add.json"), - ). - Route("POST /example.com/check", - servermock.ResponseFromInternal("zone_remove.json"), - servermock.CheckRequestJSONBodyFromInternal("zone_remove.json"), - ). - Route("PUT /example.com", - servermock.Noop(). - WithStatusCode(http.StatusNoContent), - servermock.CheckRequestJSONBodyFromInternal("zone_remove.json"), - ). - Build(t) - - err := provider.CleanUp("example.com", "abc", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/eurodns/internal/client.go b/providers/dns/eurodns/internal/client.go deleted file mode 100644 index 1ebf8d143..000000000 --- a/providers/dns/eurodns/internal/client.go +++ /dev/null @@ -1,199 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" -) - -const defaultBaseURL = "https://rest-api.eurodns.com/dns-zones/" - -const ( - HeaderAppID = "X-APP-ID" - HeaderAPIKey = "X-API-KEY" -) - -// Client the EuroDNS API client. -type Client struct { - appID string - apiKey string - - BaseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(appID, apiKey string) (*Client, error) { - if appID == "" || apiKey == "" { - return nil, errors.New("credentials missing") - } - - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - appID: appID, - apiKey: apiKey, - BaseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -// GetZone gets a DNS Zone. -// https://docapi.eurodns.com/#/dnsprovider/getdnszone -func (c *Client) GetZone(ctx context.Context, domain string) (*Zone, error) { - endpoint := c.BaseURL.JoinPath(domain) - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - result := &Zone{} - - err = c.do(req, result) - if err != nil { - return nil, err - } - - return result, nil -} - -// SaveZone saves a DNS Zone. -// https://docapi.eurodns.com/#/dnsprovider/savednszone -func (c *Client) SaveZone(ctx context.Context, domain string, zone *Zone) error { - endpoint := c.BaseURL.JoinPath(domain) - - if len(zone.URLForwards) == 0 { - zone.URLForwards = make([]URLForward, 0) - } - - if len(zone.MailForwards) == 0 { - zone.MailForwards = make([]MailForward, 0) - } - - req, err := newJSONRequest(ctx, http.MethodPut, endpoint, zone) - if err != nil { - return err - } - - return c.do(req, nil) -} - -// ValidateZone validates DNS Zone. -// https://docapi.eurodns.com/#/dnsprovider/checkdnszone -func (c *Client) ValidateZone(ctx context.Context, domain string, zone *Zone) (*Zone, error) { - endpoint := c.BaseURL.JoinPath(domain, "check") - - if len(zone.URLForwards) == 0 { - zone.URLForwards = make([]URLForward, 0) - } - - if len(zone.MailForwards) == 0 { - zone.MailForwards = make([]MailForward, 0) - } - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, zone) - if err != nil { - return nil, err - } - - result := &Zone{} - - err = c.do(req, result) - if err != nil { - return nil, err - } - - return result, nil -} - -func (c *Client) do(req *http.Request, result any) error { - req.Header.Set(HeaderAppID, c.appID) - req.Header.Set(HeaderAPIKey, c.apiKey) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIError - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return fmt.Errorf("%d: %w", resp.StatusCode, &errAPI) -} - -const DefaultTTL = 600 - -// TTLRounder rounds the given TTL in seconds to the next accepted value. -// Accepted TTL values are: 600, 900, 1800,3600, 7200, 14400, 21600, 43200, 86400, 172800, 432000, 604800. -func TTLRounder(ttl int) int { - for _, validTTL := range []int{DefaultTTL, 900, 1800, 3600, 7200, 14400, 21600, 43200, 86400, 172800, 432000, 604800} { - if ttl <= validTTL { - return validTTL - } - } - - return DefaultTTL -} diff --git a/providers/dns/eurodns/internal/client_test.go b/providers/dns/eurodns/internal/client_test.go deleted file mode 100644 index 68d1fda84..000000000 --- a/providers/dns/eurodns/internal/client_test.go +++ /dev/null @@ -1,310 +0,0 @@ -package internal - -import ( - "context" - "net/http" - "net/http/httptest" - "net/url" - "slices" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/go-acme/lego/v4/providers/dns/internal/ptr" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("abc", "secret") - if err != nil { - return nil, err - } - - client.HTTPClient = server.Client() - client.BaseURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(). - With(HeaderAppID, "abc"). - With(HeaderAPIKey, "secret"), - ) -} - -func TestClient_GetZone(t *testing.T) { - client := mockBuilder(). - Route("GET /example.com", - servermock.ResponseFromFixture("zone_get.json"), - ). - Build(t) - - zone, err := client.GetZone(context.Background(), "example.com") - require.NoError(t, err) - - expected := &Zone{ - Name: "example.com", - DomainConnect: true, - Records: slices.Concat([]Record{fakeARecord()}), - URLForwards: []URLForward{fakeURLForward()}, - MailForwards: []MailForward{fakeMailForward()}, - } - - assert.Equal(t, expected, zone) -} - -func TestClient_GetZone_error(t *testing.T) { - client := mockBuilder(). - Route("GET /example.com", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized), - ). - Build(t) - - _, err := client.GetZone(context.Background(), "example.com") - require.Error(t, err) - - require.EqualError(t, err, "401: INVALID_API_KEY: Invalid API Key") -} - -func TestClient_SaveZone(t *testing.T) { - client := mockBuilder(). - Route("PUT /example.com", - servermock.Noop(). - WithStatusCode(http.StatusNoContent), - servermock.CheckRequestJSONBodyFromFixture("zone_add.json"), - ). - Build(t) - - record := Record{ - Type: "TXT", - Host: "_acme-challenge", - RData: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: 600, - } - - zone := &Zone{ - Name: "example.com", - DomainConnect: true, - Records: []Record{fakeARecord(), record}, - URLForwards: []URLForward{fakeURLForward()}, - MailForwards: []MailForward{fakeMailForward()}, - } - - err := client.SaveZone(context.Background(), "example.com", zone) - require.NoError(t, err) -} - -func TestClient_SaveZone_emptyForwards(t *testing.T) { - client := mockBuilder(). - Route("PUT /example.com", - servermock.Noop(). - WithStatusCode(http.StatusNoContent), - servermock.CheckRequestJSONBodyFromFixture("zone_add_empty_forwards.json"), - ). - Build(t) - - record := Record{ - Type: "TXT", - Host: "_acme-challenge", - RData: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: 600, - } - - zone := &Zone{ - Name: "example.com", - DomainConnect: true, - Records: slices.Concat([]Record{fakeARecord(), record}), - } - - err := client.SaveZone(context.Background(), "example.com", zone) - require.NoError(t, err) -} - -func TestClient_SaveZone_error(t *testing.T) { - client := mockBuilder(). - Route("PUT /example.com", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized), - ). - Build(t) - - zone := &Zone{ - Name: "example.com", - DomainConnect: true, - Records: []Record{fakeARecord()}, - URLForwards: []URLForward{fakeURLForward()}, - MailForwards: []MailForward{fakeMailForward()}, - } - - err := client.SaveZone(context.Background(), "example.com", zone) - require.Error(t, err) - - require.EqualError(t, err, "401: INVALID_API_KEY: Invalid API Key") -} - -func TestClient_ValidateZone(t *testing.T) { - client := mockBuilder(). - Route("POST /example.com/check", - servermock.ResponseFromFixture("zone_add_validate_ok.json"), - servermock.CheckRequestJSONBodyFromFixture("zone_add.json"), - ). - Build(t) - - record := Record{ - Type: "TXT", - Host: "_acme-challenge", - RData: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: 600, - } - - zone := &Zone{ - Name: "example.com", - DomainConnect: true, - Records: []Record{fakeARecord(), record}, - URLForwards: []URLForward{fakeURLForward()}, - MailForwards: []MailForward{fakeMailForward()}, - } - - zone, err := client.ValidateZone(context.Background(), "example.com", zone) - require.NoError(t, err) - - expected := &Zone{ - Name: "example.com", - DomainConnect: true, - Records: []Record{fakeARecord(), record}, - URLForwards: []URLForward{fakeURLForward()}, - MailForwards: []MailForward{fakeMailForward()}, - Report: &Report{IsValid: true}, - } - - assert.Equal(t, expected, zone) -} - -func TestClient_ValidateZone_report(t *testing.T) { - client := mockBuilder(). - Route("POST /example.com/check", - servermock.ResponseFromFixture("zone_add_validate_ko.json"), - servermock.CheckRequestJSONBodyFromFixture("zone_add.json"), - ). - Build(t) - - record := Record{ - Type: "TXT", - Host: "_acme-challenge", - RData: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: 600, - } - - zone := &Zone{ - Name: "example.com", - DomainConnect: true, - Records: []Record{fakeARecord(), record}, - URLForwards: []URLForward{fakeURLForward()}, - MailForwards: []MailForward{fakeMailForward()}, - } - - zone, err := client.ValidateZone(context.Background(), "example.com", zone) - require.NoError(t, err) - - expected := &Zone{ - Name: "example.com", - DomainConnect: true, - Records: []Record{fakeARecord(), record}, - URLForwards: []URLForward{fakeURLForward()}, - MailForwards: []MailForward{fakeMailForward()}, - Report: fakeReport(), - } - - assert.EqualError(t, zone.Report, `record error (ERROR): "120" is not a valid TTL, URL forward error (ERROR): string, mail forward error (ERROR): string, zone error (ERROR): string`) - - assert.Equal(t, expected, zone) -} - -func TestClient_ValidateZone_error(t *testing.T) { - client := mockBuilder(). - Route("POST /example.com/check", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized), - ). - Build(t) - - zone := &Zone{ - Name: "example.com", - DomainConnect: true, - Records: []Record{fakeARecord()}, - URLForwards: []URLForward{fakeURLForward()}, - MailForwards: []MailForward{fakeMailForward()}, - } - - _, err := client.ValidateZone(context.Background(), "example.com", zone) - require.Error(t, err) - - require.EqualError(t, err, "401: INVALID_API_KEY: Invalid API Key") -} - -func fakeARecord() Record { - return Record{ - ID: 1000, - Type: "A", - Host: "@", - TTL: 600, - RData: "string", - Updated: ptr.Pointer(true), - Locked: ptr.Pointer(true), - IsDynDNS: ptr.Pointer(true), - Proxy: "ON", - } -} - -func fakeURLForward() URLForward { - return URLForward{ - ID: 2000, - ForwardType: "FRAME", - Host: "string", - URL: "string", - Title: "string", - Keywords: "string", - Description: "string", - Updated: ptr.Pointer(true), - } -} - -func fakeMailForward() MailForward { - return MailForward{ - ID: 3000, - Source: "string", - Destination: "string", - Updated: ptr.Pointer(true), - } -} - -func fakeReport() *Report { - return &Report{ - IsValid: false, - RecordErrors: []RecordError{{ - Messages: []string{`"120" is not a valid TTL`}, - Severity: "ERROR", - Record: fakeARecord(), - }}, - URLForwardErrors: []URLForwardError{{ - Messages: []string{"string"}, - Severity: "ERROR", - URLForward: fakeURLForward(), - }}, - MailForwardErrors: []MailForwardError{{ - Messages: []string{"string"}, - MailForward: fakeMailForward(), - Severity: "ERROR", - }}, - ZoneErrors: []ZoneError{{ - Message: "string", - Severity: "ERROR", - Records: []Record{fakeARecord()}, - URLForwards: []URLForward{fakeURLForward()}, - MailForwards: []MailForward{fakeMailForward()}, - }}, - } -} diff --git a/providers/dns/eurodns/internal/fixtures/error.json b/providers/dns/eurodns/internal/fixtures/error.json deleted file mode 100644 index 82a334598..000000000 --- a/providers/dns/eurodns/internal/fixtures/error.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "errors": [ - { - "code": "INVALID_API_KEY", - "title": "Invalid API Key" - } - ] -} diff --git a/providers/dns/eurodns/internal/fixtures/zone_add.json b/providers/dns/eurodns/internal/fixtures/zone_add.json deleted file mode 100644 index db8142357..000000000 --- a/providers/dns/eurodns/internal/fixtures/zone_add.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "example.com", - "domainConnect": true, - "records": [ - { - "id": 1000, - "type": "A", - "host": "@", - "ttl": 600, - "rdata": "string", - "updated": true, - "locked": true, - "isDynDns": true, - "proxy": "ON" - }, - { - "type": "TXT", - "host": "_acme-challenge", - "ttl": 600, - "rdata": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "updated": null, - "locked": null, - "isDynDns": null - } - ], - "urlForwards": [ - { - "id": 2000, - "forwardType": "FRAME", - "host": "string", - "url": "string", - "title": "string", - "keywords": "string", - "description": "string", - "updated": true - } - ], - "mailForwards": [ - { - "id": 3000, - "source": "string", - "destination": "string", - "updated": true - } - ] -} diff --git a/providers/dns/eurodns/internal/fixtures/zone_add_empty_forwards.json b/providers/dns/eurodns/internal/fixtures/zone_add_empty_forwards.json deleted file mode 100644 index 64f8530c9..000000000 --- a/providers/dns/eurodns/internal/fixtures/zone_add_empty_forwards.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "example.com", - "domainConnect": true, - "records": [ - { - "id": 1000, - "type": "A", - "host": "@", - "ttl": 600, - "rdata": "string", - "updated": true, - "locked": true, - "isDynDns": true, - "proxy": "ON" - }, - { - "type": "TXT", - "host": "_acme-challenge", - "ttl": 600, - "rdata": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "updated": null, - "locked": null, - "isDynDns": null - } - ], - "urlForwards": [], - "mailForwards": [] -} diff --git a/providers/dns/eurodns/internal/fixtures/zone_add_validate_ko.json b/providers/dns/eurodns/internal/fixtures/zone_add_validate_ko.json deleted file mode 100644 index e07d42299..000000000 --- a/providers/dns/eurodns/internal/fixtures/zone_add_validate_ko.json +++ /dev/null @@ -1,139 +0,0 @@ -{ - "name": "example.com", - "domainConnect": true, - "records": [ - { - "id": 1000, - "type": "A", - "host": "@", - "ttl": 600, - "rdata": "string", - "updated": true, - "locked": true, - "isDynDns": true, - "proxy": "ON" - }, - { - "type": "TXT", - "host": "_acme-challenge", - "ttl": 600, - "rdata": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "updated": null, - "locked": null, - "isDynDns": null - } - ], - "urlForwards": [ - { - "id": 2000, - "forwardType": "FRAME", - "host": "string", - "url": "string", - "title": "string", - "keywords": "string", - "description": "string", - "updated": true - } - ], - "mailForwards": [ - { - "id": 3000, - "source": "string", - "destination": "string", - "updated": true - } - ], - "report": { - "isValid": false, - "recordErrors": [ - { - "messages": [ - "\"120\" is not a valid TTL" - ], - "record": { - "id": 1000, - "type": "A", - "host": "@", - "ttl": 600, - "rdata": "string", - "updated": true, - "locked": true, - "isDynDns": true, - "proxy": "ON" - }, - "severity": "ERROR" - } - ], - "urlForwardErrors": [ - { - "messages": [ - "string" - ], - "urlForward": { - "id": 2000, - "forwardType": "FRAME", - "host": "string", - "url": "string", - "title": "string", - "keywords": "string", - "description": "string", - "updated": true - }, - "severity": "ERROR" - } - ], - "mailForwardErrors": [ - { - "messages": [ - "string" - ], - "mailForward": { - "id": 3000, - "source": "string", - "destination": "string", - "updated": true - }, - "severity": "ERROR" - } - ], - "zoneErrors": [ - { - "message": "string", - "records": [ - { - "id": 1000, - "type": "A", - "host": "@", - "ttl": 600, - "rdata": "string", - "updated": true, - "locked": true, - "isDynDns": true, - "proxy": "ON" - } - ], - "urlForwards": [ - { - "id": 2000, - "forwardType": "FRAME", - "host": "string", - "url": "string", - "title": "string", - "keywords": "string", - "description": "string", - "updated": true - } - ], - "mailForwards": [ - { - "id": 3000, - "source": "string", - "destination": "string", - "updated": true - } - ], - "severity": "ERROR" - } - ] - } -} diff --git a/providers/dns/eurodns/internal/fixtures/zone_add_validate_ok.json b/providers/dns/eurodns/internal/fixtures/zone_add_validate_ok.json deleted file mode 100644 index ba0ddfefb..000000000 --- a/providers/dns/eurodns/internal/fixtures/zone_add_validate_ok.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "example.com", - "domainConnect": true, - "records": [ - { - "id": 1000, - "type": "A", - "host": "@", - "ttl": 600, - "rdata": "string", - "updated": true, - "locked": true, - "isDynDns": true, - "proxy": "ON" - }, - { - "type": "TXT", - "host": "_acme-challenge", - "ttl": 600, - "rdata": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "updated": null, - "locked": null, - "isDynDns": null - } - ], - "urlForwards": [ - { - "id": 2000, - "forwardType": "FRAME", - "host": "string", - "url": "string", - "title": "string", - "keywords": "string", - "description": "string", - "updated": true - } - ], - "mailForwards": [ - { - "id": 3000, - "source": "string", - "destination": "string", - "updated": true - } - ], - "report": { - "isValid": true - } -} diff --git a/providers/dns/eurodns/internal/fixtures/zone_get.json b/providers/dns/eurodns/internal/fixtures/zone_get.json deleted file mode 100644 index ebbc8593e..000000000 --- a/providers/dns/eurodns/internal/fixtures/zone_get.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "example.com", - "domainConnect": true, - "records": [ - { - "id": 1000, - "type": "A", - "host": "@", - "ttl": 600, - "rdata": "string", - "updated": true, - "locked": true, - "isDynDns": true, - "proxy": "ON" - } - ], - "urlForwards": [ - { - "id": 2000, - "forwardType": "FRAME", - "host": "string", - "url": "string", - "title": "string", - "keywords": "string", - "description": "string", - "updated": true - } - ], - "mailForwards": [ - { - "id": 3000, - "source": "string", - "destination": "string", - "updated": true - } - ] -} diff --git a/providers/dns/eurodns/internal/fixtures/zone_remove.json b/providers/dns/eurodns/internal/fixtures/zone_remove.json deleted file mode 100644 index ebbc8593e..000000000 --- a/providers/dns/eurodns/internal/fixtures/zone_remove.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "example.com", - "domainConnect": true, - "records": [ - { - "id": 1000, - "type": "A", - "host": "@", - "ttl": 600, - "rdata": "string", - "updated": true, - "locked": true, - "isDynDns": true, - "proxy": "ON" - } - ], - "urlForwards": [ - { - "id": 2000, - "forwardType": "FRAME", - "host": "string", - "url": "string", - "title": "string", - "keywords": "string", - "description": "string", - "updated": true - } - ], - "mailForwards": [ - { - "id": 3000, - "source": "string", - "destination": "string", - "updated": true - } - ] -} diff --git a/providers/dns/eurodns/internal/types.go b/providers/dns/eurodns/internal/types.go deleted file mode 100644 index 891b02e14..000000000 --- a/providers/dns/eurodns/internal/types.go +++ /dev/null @@ -1,136 +0,0 @@ -package internal - -import ( - "fmt" - "strings" -) - -type APIError struct { - Errors []Error `json:"errors"` -} - -func (a *APIError) Error() string { - var msg []string - - for _, e := range a.Errors { - msg = append(msg, fmt.Sprintf("%s: %s", e.Code, e.Title)) - } - - return strings.Join(msg, ", ") -} - -type Error struct { - Code string `json:"code"` - Title string `json:"title"` -} - -type Zone struct { - Name string `json:"name,omitempty"` - DomainConnect bool `json:"domainConnect,omitempty"` - Records []Record `json:"records"` - URLForwards []URLForward `json:"urlForwards"` - MailForwards []MailForward `json:"mailForwards"` - Report *Report `json:"report,omitempty"` -} - -type Record struct { - ID int `json:"id,omitempty"` - Type string `json:"type,omitempty"` - Host string `json:"host,omitempty"` - TTL int `json:"ttl,omitempty"` - RData string `json:"rdata,omitempty"` - Updated *bool `json:"updated"` - Locked *bool `json:"locked"` - IsDynDNS *bool `json:"isDynDns"` - Proxy string `json:"proxy,omitempty"` -} - -type URLForward struct { - ID int `json:"id,omitempty"` - ForwardType string `json:"forwardType,omitempty"` - Host string `json:"host,omitempty"` - URL string `json:"url,omitempty"` - Title string `json:"title,omitempty"` - Keywords string `json:"keywords,omitempty"` - Description string `json:"description,omitempty"` - Updated *bool `json:"updated,omitempty"` -} - -type MailForward struct { - ID int `json:"id,omitempty"` - Source string `json:"source,omitempty"` - Destination string `json:"destination,omitempty"` - Updated *bool `json:"updated,omitempty"` -} - -type Report struct { - IsValid bool `json:"isValid,omitempty"` - RecordErrors []RecordError `json:"recordErrors,omitempty"` - URLForwardErrors []URLForwardError `json:"urlForwardErrors,omitempty"` - MailForwardErrors []MailForwardError `json:"mailForwardErrors,omitempty"` - ZoneErrors []ZoneError `json:"zoneErrors,omitempty"` -} - -func (r *Report) Error() string { - var msg []string - - for _, e := range r.RecordErrors { - msg = append(msg, e.Error()) - } - - for _, e := range r.URLForwardErrors { - msg = append(msg, e.Error()) - } - - for _, e := range r.MailForwardErrors { - msg = append(msg, e.Error()) - } - - for _, e := range r.ZoneErrors { - msg = append(msg, e.Error()) - } - - return strings.Join(msg, ", ") -} - -type RecordError struct { - Messages []string `json:"messages,omitempty"` - Record Record `json:"record"` - Severity string `json:"severity,omitempty"` -} - -func (e *RecordError) Error() string { - return fmt.Sprintf("record error (%s): %s", e.Severity, strings.Join(e.Messages, ", ")) -} - -type URLForwardError struct { - Messages []string `json:"messages,omitempty"` - URLForward URLForward `json:"urlForward"` - Severity string `json:"severity,omitempty"` -} - -func (e *URLForwardError) Error() string { - return fmt.Sprintf("URL forward error (%s): %s", e.Severity, strings.Join(e.Messages, ", ")) -} - -type MailForwardError struct { - Messages []string `json:"messages,omitempty"` - MailForward MailForward `json:"mailForward"` - Severity string `json:"severity,omitempty"` -} - -func (e *MailForwardError) Error() string { - return fmt.Sprintf("mail forward error (%s): %s", e.Severity, strings.Join(e.Messages, ", ")) -} - -type ZoneError struct { - Message string `json:"message,omitempty"` - Records []Record `json:"records,omitempty"` - URLForwards []URLForward `json:"urlForwards,omitempty"` - MailForwards []MailForward `json:"mailForwards,omitempty"` - Severity string `json:"severity,omitempty"` -} - -func (e *ZoneError) Error() string { - return fmt.Sprintf("zone error (%s): %s", e.Severity, e.Message) -} diff --git a/providers/dns/excedo/excedo.go b/providers/dns/excedo/excedo.go deleted file mode 100644 index ae9128b94..000000000 --- a/providers/dns/excedo/excedo.go +++ /dev/null @@ -1,176 +0,0 @@ -// Package excedo implements a DNS provider for solving the DNS-01 challenge using Excedo. -package excedo - -import ( - "context" - "errors" - "fmt" - "net/http" - "strconv" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/excedo/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "EXCEDO_" - - EnvAPIURL = envNamespace + "API_URL" - EnvAPIKey = envNamespace + "API_KEY" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIURL string - APIKey string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, 60), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client - - recordsMu sync.Mutex - records map[string]int64 -} - -// NewDNSProvider returns a DNSProvider instance configured for Excedo. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIURL, EnvAPIKey) - if err != nil { - return nil, fmt.Errorf("excedo: %w", err) - } - - config := NewDefaultConfig() - config.APIURL = values[EnvAPIURL] - config.APIKey = values[EnvAPIKey] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Excedo. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("excedo: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.APIURL, config.APIKey) - if err != nil { - return nil, fmt.Errorf("excedo: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - records: make(map[string]int64), - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("excedo: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("excedo: %w", err) - } - - record := internal.Record{ - DomainName: dns01.UnFqdn(authZone), - Name: subDomain, - Type: "TXT", - Content: info.Value, - TTL: strconv.Itoa(d.config.TTL), - } - - recordID, err := d.client.AddRecord(ctx, record) - if err != nil { - return fmt.Errorf("excedo: add record: %w", err) - } - - d.recordsMu.Lock() - d.records[token] = recordID - d.recordsMu.Unlock() - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("excedo: could not find zone for domain %q: %w", domain, err) - } - - d.recordsMu.Lock() - recordID, ok := d.records[token] - d.recordsMu.Unlock() - - if !ok { - return fmt.Errorf("excedo: unknown record ID for '%s'", info.EffectiveFQDN) - } - - err = d.client.DeleteRecord(ctx, dns01.UnFqdn(authZone), strconv.FormatInt(recordID, 10)) - if err != nil { - return fmt.Errorf("excedo: delete record: %w", err) - } - - d.recordsMu.Lock() - delete(d.records, token) - d.recordsMu.Unlock() - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/excedo/excedo.toml b/providers/dns/excedo/excedo.toml deleted file mode 100644 index 9f9874c62..000000000 --- a/providers/dns/excedo/excedo.toml +++ /dev/null @@ -1,24 +0,0 @@ -Name = "Excedo" -Description = '''''' -URL = "https://excedo.se/" -Code = "excedo" -Since = "v4.33.0" - -Example = ''' -EXCEDO_API_KEY=your-api-key \ -EXCEDO_API_URL=your-base-url \ -lego --dns excedo -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - EXCEDO_API_KEY = "API key" - EXCEDO_API_URL = "API base URL" - [Configuration.Additional] - EXCEDO_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" - EXCEDO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 300)" - EXCEDO_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" - EXCEDO_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "none" diff --git a/providers/dns/excedo/excedo_test.go b/providers/dns/excedo/excedo_test.go deleted file mode 100644 index f2350c035..000000000 --- a/providers/dns/excedo/excedo_test.go +++ /dev/null @@ -1,210 +0,0 @@ -package excedo - -import ( - "net/http/httptest" - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAPIURL, EnvAPIKey).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAPIURL: "https://example.com", - EnvAPIKey: "secret", - }, - }, - { - desc: "missing the API key", - envVars: map[string]string{ - EnvAPIURL: "https://example.com", - EnvAPIKey: "", - }, - expected: "excedo: some credentials information are missing: EXCEDO_API_KEY", - }, - { - desc: "missing the API URL", - envVars: map[string]string{ - EnvAPIURL: "", - EnvAPIKey: "secret", - }, - expected: "excedo: some credentials information are missing: EXCEDO_API_URL", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "excedo: some credentials information are missing: EXCEDO_API_URL,EXCEDO_API_KEY", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiURL string - apiKey string - expected string - }{ - { - desc: "success", - apiURL: "https://example.com", - apiKey: "secret", - }, - { - desc: "missing the API key", - apiURL: "https://example.com", - expected: "excedo: credentials missing", - }, - { - desc: "missing the API URL", - apiKey: "secret", - expected: "excedo: credentials missing", - }, - { - desc: "missing credentials", - expected: "excedo: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIURL = test.apiURL - config.APIKey = test.apiKey - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIURL = server.URL - config.APIKey = "secret" - config.HTTPClient = server.Client() - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - return p, nil - }, - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("GET /authenticate/login/", - servermock.ResponseFromInternal("login.json"), - servermock.CheckHeader(). - WithAuthorization("Bearer secret"), - ). - Route("POST /dns/addrecord/", - servermock.ResponseFromInternal("addrecord.json"), - servermock.CheckHeader(). - WithAuthorization("Bearer session-token"), - servermock.CheckForm().Strict(). - With("content", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"). - With("domainName", "example.com"). - With("name", "_acme-challenge"). - With("ttl", "60"). - With("type", "TXT"), - ). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("GET /authenticate/login/", - servermock.ResponseFromInternal("login.json"), - servermock.CheckHeader(). - WithAuthorization("Bearer secret"), - ). - Route("POST /dns/deleterecord/", - servermock.ResponseFromInternal("deleterecord.json"), - servermock.CheckHeader(). - WithAuthorization("Bearer session-token"), - ). - Build(t) - - provider.records["abc"] = 19695822 - - err := provider.CleanUp("example.com", "abc", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/excedo/internal/client.go b/providers/dns/excedo/internal/client.go deleted file mode 100644 index a5d8be88b..000000000 --- a/providers/dns/excedo/internal/client.go +++ /dev/null @@ -1,205 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "mime/multipart" - "net/http" - "net/url" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" - querystring "github.com/google/go-querystring/query" -) - -type responseChecker interface { - Check() error -} - -// Client the Excedo API client. -type Client struct { - apiKey string - - baseURL *url.URL - HTTPClient *http.Client - - token *ExpirableToken - muToken sync.Mutex -} - -// NewClient creates a new Client. -func NewClient(apiURL, apiKey string) (*Client, error) { - if apiURL == "" || apiKey == "" { - return nil, errors.New("credentials missing") - } - - baseURL, err := url.Parse(apiURL) - if err != nil { - return nil, err - } - - return &Client{ - apiKey: apiKey, - baseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -func (c *Client) AddRecord(ctx context.Context, record Record) (int64, error) { - payload, err := querystring.Values(record) - if err != nil { - return 0, err - } - - endpoint := c.baseURL.JoinPath("/dns/addrecord/") - - req, err := newFormRequest(ctx, http.MethodPost, endpoint, payload) - if err != nil { - return 0, err - } - - result := new(AddRecordResponse) - - err = c.doAuthenticated(ctx, req, result) - if err != nil { - return 0, err - } - - return result.RecordID, nil -} - -func (c *Client) DeleteRecord(ctx context.Context, zone, recordID string) error { - endpoint := c.baseURL.JoinPath("/dns/deleterecord/") - - data := map[string]string{ - "domainname": dns01.UnFqdn(zone), - "recordid": recordID, - } - - req, err := newMultipartRequest(ctx, http.MethodPost, endpoint, data) - if err != nil { - return err - } - - result := new(BaseResponse) - - err = c.doAuthenticated(ctx, req, result) - if err != nil { - return err - } - - return nil -} - -func (c *Client) GetRecords(ctx context.Context, zone string) (map[string]Zone, error) { - endpoint := c.baseURL.JoinPath("/dns/getrecords/") - - query := endpoint.Query() - query.Set("domainname", zone) - - endpoint.RawQuery = query.Encode() - - req, err := newFormRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - result := new(GetRecordsResponse) - - err = c.doAuthenticated(ctx, req, result) - if err != nil { - return nil, err - } - - return result.DNS, nil -} - -func (c *Client) do(req *http.Request, result responseChecker) error { - useragent.SetHeader(req.Header) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - raw, _ := io.ReadAll(resp.Body) - - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return result.Check() -} - -func newMultipartRequest(ctx context.Context, method string, endpoint *url.URL, data map[string]string) (*http.Request, error) { - buf := new(bytes.Buffer) - - writer := multipart.NewWriter(buf) - - for k, v := range data { - err := writer.WriteField(k, v) - if err != nil { - return nil, err - } - } - - err := writer.Close() - if err != nil { - return nil, err - } - - body := bytes.NewReader(buf.Bytes()) - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Content-Type", writer.FormDataContentType()) - - return req, nil -} - -func newFormRequest(ctx context.Context, method string, endpoint *url.URL, form url.Values) (*http.Request, error) { - var body io.Reader - - if len(form) > 0 { - body = bytes.NewReader([]byte(form.Encode())) - } else { - body = http.NoBody - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - if method == http.MethodPost { - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - } - - return req, nil -} diff --git a/providers/dns/excedo/internal/client_test.go b/providers/dns/excedo/internal/client_test.go deleted file mode 100644 index f4fd52c00..000000000 --- a/providers/dns/excedo/internal/client_test.go +++ /dev/null @@ -1,137 +0,0 @@ -package internal - -import ( - "net/http/httptest" - "testing" - "time" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient(server.URL, "secret") - if err != nil { - return nil, err - } - - client.HTTPClient = server.Client() - - return client, nil - }, - ) -} - -func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /dns/addrecord/", - servermock.ResponseFromFixture("addrecord.json"), - servermock.CheckHeader(). - WithAuthorization("Bearer session-token"), - servermock.CheckForm().Strict(). - With("content", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"). - With("domainName", "example.com"). - With("name", "_acme-challenge"). - With("ttl", "60"). - With("type", "TXT"), - ). - Build(t) - - client.token = &ExpirableToken{ - Token: "session-token", - Expires: time.Now().Add(6 * time.Hour), - } - - record := Record{ - DomainName: "example.com", - Name: "_acme-challenge", - Type: "TXT", - Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: "60", - } - - recordID, err := client.AddRecord(t.Context(), record) - require.NoError(t, err) - - assert.EqualValues(t, 19695822, recordID) -} - -func TestClient_AddRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /dns/addrecord/", - servermock.ResponseFromFixture("error.json"), - ). - Build(t) - - client.token = &ExpirableToken{ - Token: "session-token", - Expires: time.Now().Add(6 * time.Hour), - } - - record := Record{ - DomainName: "example.com", - Name: "_acme-challenge", - Type: "TXT", - Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: "60", - } - - _, err := client.AddRecord(t.Context(), record) - require.EqualError(t, err, "2003: Required parameter missing") -} - -func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /dns/deleterecord/", - servermock.ResponseFromFixture("deleterecord.json"), - servermock.CheckHeader(). - WithAuthorization("Bearer session-token"), - ). - Build(t) - - client.token = &ExpirableToken{ - Token: "session-token", - Expires: time.Now().Add(6 * time.Hour), - } - - err := client.DeleteRecord(t.Context(), "example.com", "19695822") - require.NoError(t, err) -} - -func TestClient_GetRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/getrecords/", - servermock.ResponseFromFixture("getrecords.json"), - servermock.CheckHeader(). - WithAuthorization("Bearer session-token"), - servermock.CheckQueryParameter().Strict(). - With("domainname", "example.com"), - ). - Build(t) - - client.token = &ExpirableToken{ - Token: "session-token", - Expires: time.Now().Add(6 * time.Hour), - } - - zones, err := client.GetRecords(t.Context(), "example.com") - require.NoError(t, err) - - expected := map[string]Zone{ - "example.com": { - DNSType: "type", - Records: []Record{{ - RecordID: "1234", - Name: "_acme-challenge.example.com", - Type: "TXT", - Content: "txt-value", - TTL: "60", - }}, - }, - } - - assert.Equal(t, expected, zones) -} diff --git a/providers/dns/excedo/internal/fixtures/addrecord.json b/providers/dns/excedo/internal/fixtures/addrecord.json deleted file mode 100644 index f1f7bf958..000000000 --- a/providers/dns/excedo/internal/fixtures/addrecord.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "code": 1000, - "desc": "Command completed successfully", - "recordid": 19695822, - "session": { - "accID": "1234", - "usrID": "1234", - "status": "active", - "expire": { - "date": "2026-03-10 19:03:18", - "seconds": 5678 - } - }, - "runtime": 0.2852 -} diff --git a/providers/dns/excedo/internal/fixtures/deleterecord.json b/providers/dns/excedo/internal/fixtures/deleterecord.json deleted file mode 100644 index 5c2431b1c..000000000 --- a/providers/dns/excedo/internal/fixtures/deleterecord.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "code": 1000, - "desc": "Command completed successfully", - "session": { - "accID": "1234", - "usrID": "1234", - "status": "active", - "expire": { - "date": "2026-03-10 19:03:18", - "seconds": 5678 - } - }, - "runtime": 0.2852 -} diff --git a/providers/dns/excedo/internal/fixtures/error.json b/providers/dns/excedo/internal/fixtures/error.json deleted file mode 100644 index 5a24ec247..000000000 --- a/providers/dns/excedo/internal/fixtures/error.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "code": 2003, - "desc": "Required parameter missing", - "missing": [ - "domainname", - "recordid" - ], - "session": { - "accID": "1234", - "usrID": "1234", - "status": "active", - "expire": { - "date": "2026-03-10 19:03:18", - "seconds": 5485 - } - }, - "runtime": 0.0534 -} diff --git a/providers/dns/excedo/internal/fixtures/getrecords.json b/providers/dns/excedo/internal/fixtures/getrecords.json deleted file mode 100644 index 215a8abb2..000000000 --- a/providers/dns/excedo/internal/fixtures/getrecords.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "code": 1000, - "desc": "Command completed successfully", - "dns": { - "example.com": { - "dnstype": "type", - "recordusage": { - "used": 74 - }, - "records": [ - { - "recordid": "1234", - "name": "_acme-challenge.example.com", - "type": "TXT", - "content": "txt-value", - "ttl": "60", - "prio": null, - "change_date": null - } - ] - } - } -} diff --git a/providers/dns/excedo/internal/fixtures/login.json b/providers/dns/excedo/internal/fixtures/login.json deleted file mode 100644 index 2defb9843..000000000 --- a/providers/dns/excedo/internal/fixtures/login.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "code": 1000, - "desc": "Command completed successfully", - "parameters": { - "token": "session-token" - } -} diff --git a/providers/dns/excedo/internal/identity.go b/providers/dns/excedo/internal/identity.go deleted file mode 100644 index 5c9ca119d..000000000 --- a/providers/dns/excedo/internal/identity.go +++ /dev/null @@ -1,75 +0,0 @@ -package internal - -import ( - "context" - "fmt" - "net/http" - "time" -) - -type ExpirableToken struct { - Token string - Expires time.Time -} - -func (t *ExpirableToken) IsExpired() bool { - return time.Now().After(t.Expires) -} - -func (c *Client) Login(ctx context.Context) (string, error) { - endpoint := c.baseURL.JoinPath("/authenticate/login/") - - req, err := newFormRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return "", err - } - - req.Header.Set("Authorization", "Bearer "+c.apiKey) - - result := new(LoginResponse) - - err = c.do(req, result) - if err != nil { - return "", err - } - - if result.Code != 1000 && result.Code != 1300 { - return "", fmt.Errorf("%d: %s", result.Code, result.Description) - } - - return result.Parameters.Token, nil -} - -func (c *Client) authenticate(ctx context.Context) (string, error) { - c.muToken.Lock() - defer c.muToken.Unlock() - - if c.token == nil || c.token.IsExpired() { - token, err := c.Login(ctx) - if err != nil { - return "", err - } - - c.token = &ExpirableToken{ - Token: token, - Expires: time.Now().Add(2*time.Hour - time.Minute), - } - - return token, nil - } - - return c.token.Token, nil -} - -func (c *Client) doAuthenticated(ctx context.Context, req *http.Request, result responseChecker) error { - token, err := c.authenticate(ctx) - if err != nil { - return err - } - - if token != "" { - req.Header.Set("Authorization", "Bearer "+token) - } - - return c.do(req, result) -} diff --git a/providers/dns/excedo/internal/identity_test.go b/providers/dns/excedo/internal/identity_test.go deleted file mode 100644 index 86b7eb9d8..000000000 --- a/providers/dns/excedo/internal/identity_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package internal - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestClient_Login(t *testing.T) { - client := mockBuilder(). - Route("GET /authenticate/login/", - servermock.ResponseFromFixture("login.json"), - servermock.CheckHeader(). - WithAuthorization("Bearer secret"), - ). - Build(t) - - token, err := client.Login(t.Context()) - require.NoError(t, err) - - assert.Equal(t, "session-token", token) -} - -func TestClient_Login_error(t *testing.T) { - client := mockBuilder(). - Route("GET /authenticate/login/", - servermock.ResponseFromFixture("error.json"), - ). - Build(t) - - _, err := client.Login(t.Context()) - require.EqualError(t, err, "2003: Required parameter missing") -} diff --git a/providers/dns/excedo/internal/types.go b/providers/dns/excedo/internal/types.go deleted file mode 100644 index eb6ce8462..000000000 --- a/providers/dns/excedo/internal/types.go +++ /dev/null @@ -1,65 +0,0 @@ -package internal - -import "fmt" - -type BaseResponse struct { - Code int `json:"code"` - Description string `json:"desc"` -} - -func (r BaseResponse) Check() error { - // Response codes: - // - 1000: Command completed successfully - // - 1300: Command completed successfully; no messages - // - 2001: Command syntax error - // - 2002: Command use error - // - 2003: Required parameter missing - // - 2004: Parameter value range error - // - 2104: Billing failure - // - 2200: Authentication error - // - 2201: Authorization error - // - 2303: Object does not exist - // - 2304: Object status prohibits operation - // - 2309: Object duplicate found - // - 2400: Command failed - // - 2500: Command failed; server closing connection - if r.Code != 1000 && r.Code != 1300 { - return fmt.Errorf("%d: %s", r.Code, r.Description) - } - - return nil -} - -type GetRecordsResponse struct { - BaseResponse - - DNS map[string]Zone `json:"dns"` -} - -type Zone struct { - DNSType string `json:"dnstype"` - Records []Record `json:"records"` -} - -type Record struct { - DomainName string `json:"domainName,omitempty" url:"domainName,omitempty"` - RecordID string `json:"recordid,omitempty" url:"recordid,omitempty"` - Name string `json:"name,omitempty" url:"name,omitempty"` - Type string `json:"type,omitempty" url:"type,omitempty"` - Content string `json:"content,omitempty" url:"content,omitempty"` - TTL string `json:"ttl,omitempty" url:"ttl,omitempty"` -} - -type AddRecordResponse struct { - BaseResponse - - RecordID int64 `json:"recordid"` -} - -type LoginResponse struct { - BaseResponse - - Parameters struct { - Token string `json:"token"` - } `json:"parameters"` -} diff --git a/providers/dns/exec/exec.go b/providers/dns/exec/exec.go index 9f000b80d..23fdaf384 100644 --- a/providers/dns/exec/exec.go +++ b/providers/dns/exec/exec.go @@ -10,7 +10,6 @@ import ( "os/exec" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" @@ -28,8 +27,6 @@ const ( EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config Provider configuration. type Config struct { Program string diff --git a/providers/dns/exec/exec.toml b/providers/dns/exec/exec.toml index 2f9c77c67..e5868d601 100644 --- a/providers/dns/exec/exec.toml +++ b/providers/dns/exec/exec.toml @@ -6,7 +6,7 @@ Since = "v0.5.0" Example = ''' EXEC_PATH=/the/path/to/myscript.sh \ -lego --dns exec -d '*.example.com' -d example.com run +lego --email you@example.com --dns exec --domains my.example.org run ''' Additional = ''' @@ -21,11 +21,11 @@ Additional = ''' ## Additional Configuration -| Environment Variable Name | Description | -|----------------------------|--------------------------------------------------------------------| -| `EXEC_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 3). | -| `EXEC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60). | -| `EXEC_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60). | +| Environment Variable Name | Description | +|----------------------------|-------------------------------------------| +| `EXEC_POLLING_INTERVAL` | Time between DNS propagation check. | +| `EXEC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation. | +| `EXEC_SEQUENCE_INTERVAL` | Time between sequential requests. | ## Description @@ -39,7 +39,9 @@ For example, requesting a certificate for the domain 'my.example.org' can be ach ```bash EXEC_PATH=./update-dns.sh \ -lego --dns exec --d my.example.org run + lego --email you@example.com \ + --dns exec \ + --domains my.example.org run ``` It will then call the program './update-dns.sh' with like this: @@ -59,7 +61,9 @@ If you want to use the raw domain, token, and keyAuth values with your program, ```bash EXEC_MODE=RAW \ EXEC_PATH=./update-dns.sh \ -lego --dns exec -d my.example.org run + lego --email you@example.com \ + --dns exec \ + --domains my.example.org run ``` It will then call the program `./update-dns.sh` like this: diff --git a/providers/dns/exec/exec_test.go b/providers/dns/exec/exec_test.go index c1b6da55e..3a2edbbf4 100644 --- a/providers/dns/exec/exec_test.go +++ b/providers/dns/exec/exec_test.go @@ -14,7 +14,6 @@ import ( func TestDNSProvider_Present(t *testing.T) { backupLogger := log.Logger - defer func() { log.Logger = backupLogger }() @@ -63,7 +62,6 @@ func TestDNSProvider_Present(t *testing.T) { } var message string - logRecorder.On("Println", mock.Anything).Run(func(args mock.Arguments) { message = args.String(0) fmt.Fprintln(os.Stdout, "XXX", message) @@ -89,7 +87,6 @@ func TestDNSProvider_Present(t *testing.T) { func TestDNSProvider_CleanUp(t *testing.T) { backupLogger := log.Logger - defer func() { log.Logger = backupLogger }() @@ -138,7 +135,6 @@ func TestDNSProvider_CleanUp(t *testing.T) { } var message string - logRecorder.On("Println", mock.Anything).Run(func(args mock.Arguments) { message = args.String(0) fmt.Fprintln(os.Stdout, "XXX", message) diff --git a/providers/dns/exec/log_mock_test.go b/providers/dns/exec/log_mock_test.go index 65753dcf8..47935cc55 100644 --- a/providers/dns/exec/log_mock_test.go +++ b/providers/dns/exec/log_mock_test.go @@ -6,26 +6,26 @@ type LogRecorder struct { mock.Mock } -func (*LogRecorder) Fatal(args ...any) { +func (*LogRecorder) Fatal(args ...interface{}) { panic("implement me") } -func (*LogRecorder) Fatalln(args ...any) { +func (*LogRecorder) Fatalln(args ...interface{}) { panic("implement me") } -func (*LogRecorder) Fatalf(format string, args ...any) { +func (*LogRecorder) Fatalf(format string, args ...interface{}) { panic("implement me") } -func (*LogRecorder) Print(args ...any) { +func (*LogRecorder) Print(args ...interface{}) { panic("implement me") } -func (l *LogRecorder) Println(args ...any) { +func (l *LogRecorder) Println(args ...interface{}) { l.Called(args...) } -func (*LogRecorder) Printf(format string, args ...any) { +func (*LogRecorder) Printf(format string, args ...interface{}) { panic("implement me") } diff --git a/providers/dns/exoscale/exoscale.go b/providers/dns/exoscale/exoscale.go index 05fcb6a6f..cea29aa7e 100644 --- a/providers/dns/exoscale/exoscale.go +++ b/providers/dns/exoscale/exoscale.go @@ -5,19 +5,20 @@ import ( "context" "errors" "fmt" - "net/http" - "strconv" "time" - egoscale "github.com/exoscale/egoscale/v3" - "github.com/exoscale/egoscale/v3/credentials" - "github.com/go-acme/lego/v4/challenge" + egoscale "github.com/exoscale/egoscale/v2" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" ) +// Default Exoscale API endpoint. +const defaultBaseURL = "https://api.exoscale.com/v2" + +// Default Exosacle API zone. +// Each data center location hosts the API and API zone determines which one to connect to. +const defaultAPIZone = "ch-gva-2" + // Environment variables names. const ( envNamespace = "EXOSCALE_" @@ -25,6 +26,7 @@ const ( EnvAPISecret = envNamespace + "API_SECRET" EnvAPIKey = envNamespace + "API_KEY" EnvEndpoint = envNamespace + "ENDPOINT" + EnvAPIZone = envNamespace + "API_ZONE" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" @@ -32,8 +34,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string @@ -57,8 +57,9 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - config *Config - client *egoscale.Client + config *Config + client *egoscale.Client + apiZone string } // NewDNSProvider Credentials must be passed in the environment variables: @@ -72,7 +73,7 @@ func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] config.APISecret = values[EnvAPISecret] - config.Endpoint = env.GetOrDefaultString(EnvEndpoint, string(egoscale.CHGva2)) + config.Endpoint = env.GetOrFile(EnvEndpoint) return NewDNSProviderConfig(config) } @@ -87,26 +88,30 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("exoscale: credentials missing") } + if config.Endpoint == "" { + config.Endpoint = defaultBaseURL + } + client, err := egoscale.NewClient( - credentials.NewStaticCredentials(config.APIKey, config.APISecret), - egoscale.ClientOptWithEndpoint(egoscale.Endpoint(config.Endpoint)), - egoscale.ClientOptWithHTTPClient(clientdebug.Wrap(&http.Client{Timeout: config.HTTPTimeout})), - egoscale.ClientOptWithUserAgent(useragent.Get()), + config.APIKey, + config.APISecret, + egoscale.ClientOptWithAPIEndpoint(config.Endpoint), + egoscale.ClientOptWithTimeout(config.HTTPTimeout), ) if err != nil { return nil, fmt.Errorf("exoscale: initializing client: %w", err) } return &DNSProvider{ - client: client, - config: config, + client: client, + config: config, + apiZone: env.GetOrDefaultString(EnvAPIZone, defaultAPIZone), }, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) zoneName, recordName, err := d.findZoneAndRecordName(info.EffectiveFQDN) @@ -114,28 +119,22 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("exoscale: %w", err) } - zone, err := d.findExistingZone(ctx, zoneName) + zone, err := d.findExistingZone(zoneName) if err != nil { return fmt.Errorf("exoscale: %w", err) } - if zone == nil { return fmt.Errorf("exoscale: zone %q not found", zoneName) } - recordRequest := egoscale.CreateDNSDomainRecordRequest{ - Name: recordName, - Ttl: d.config.TTL, - Content: info.Value, - Type: egoscale.CreateDNSDomainRecordRequestTypeTXT, + record := egoscale.DNSDomainRecord{ + Name: pointer(recordName), + TTL: pointer(d.config.TTL), + Content: pointer(info.Value), + Type: pointer("TXT"), } - op, err := d.client.CreateDNSDomainRecord(ctx, zone.ID, recordRequest) - if err != nil { - return fmt.Errorf("exoscale: error while creating DNS record: %w", err) - } - - _, err = d.client.Wait(ctx, op, egoscale.OperationStateSuccess) + _, err = d.client.CreateDNSDomainRecord(ctx, d.apiZone, deref(zone.ID), &record) if err != nil { return fmt.Errorf("exoscale: error while creating DNS record: %w", err) } @@ -146,7 +145,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) zoneName, recordName, err := d.findZoneAndRecordName(info.EffectiveFQDN) @@ -154,32 +152,24 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("exoscale: %w", err) } - zone, err := d.findExistingZone(ctx, zoneName) + zone, err := d.findExistingZone(zoneName) if err != nil { return fmt.Errorf("exoscale: %w", err) } - if zone == nil { return fmt.Errorf("exoscale: zone %q not found", zoneName) } - recordID, err := d.findExistingRecordID(ctx, zone.ID, recordName, info.Value) + recordID, err := d.findExistingRecordID(deref(zone.ID), recordName, info.Value) if err != nil { return err } - if recordID == "" { - return nil - } - - op, err := d.client.DeleteDNSDomainRecord(ctx, zone.ID, recordID) - if err != nil { - return fmt.Errorf("exoscale: error while deleting DNS record: %w", err) - } - - _, err = d.client.Wait(ctx, op, egoscale.OperationStateSuccess) - if err != nil { - return fmt.Errorf("exoscale: error while creating DNS record: %w", err) + if recordID != "" { + err = d.client.DeleteDNSDomainRecord(ctx, d.apiZone, deref(zone.ID), &egoscale.DNSDomainRecord{ID: &recordID}) + if err != nil { + return fmt.Errorf("exoscale: error while deleting DNS record: %w", err) + } } return nil @@ -193,14 +183,16 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // findExistingZone Query Exoscale to find an existing zone for this name. // Returns nil result if no zone could be found. -func (d *DNSProvider) findExistingZone(ctx context.Context, zoneName string) (*egoscale.DNSDomain, error) { - zones, err := d.client.ListDNSDomains(ctx) +func (d *DNSProvider) findExistingZone(zoneName string) (*egoscale.DNSDomain, error) { + ctx := context.Background() + + zones, err := d.client.ListDNSDomains(ctx, d.apiZone) if err != nil { return nil, fmt.Errorf("error while retrieving DNS zones: %w", err) } - for _, zone := range zones.DNSDomains { - if zone.UnicodeName == zoneName { + for _, zone := range zones { + if zone.UnicodeName != nil && deref(zone.UnicodeName) == zoneName { return &zone, nil } } @@ -210,16 +202,17 @@ func (d *DNSProvider) findExistingZone(ctx context.Context, zoneName string) (*e // findExistingRecordID Query Exoscale to find an existing record for this name. // Returns empty result if no record could be found. -func (d *DNSProvider) findExistingRecordID(ctx context.Context, zoneID egoscale.UUID, recordName, value string) (egoscale.UUID, error) { - records, err := d.client.ListDNSDomainRecords(ctx, zoneID) +func (d *DNSProvider) findExistingRecordID(zoneID, recordName, value string) (string, error) { + ctx := context.Background() + + records, err := d.client.ListDNSDomainRecords(ctx, d.apiZone, zoneID) if err != nil { return "", fmt.Errorf("error while retrieving DNS records: %w", err) } - for _, record := range records.DNSDomainRecords { - if record.Name == recordName && record.Type == egoscale.DNSDomainRecordTypeTXT && - (record.Content == value || record.Content == strconv.Quote(value)) { - return record.ID, nil + for _, record := range records { + if deref(record.Name) == recordName && deref(record.Type) == "TXT" && deref(record.Content) == value { + return deref(record.ID), nil } } @@ -242,3 +235,14 @@ func (d *DNSProvider) findZoneAndRecordName(fqdn string) (string, string, error) return zone, subDomain, nil } + +func pointer[T string | int | int32 | int64](v T) *T { return &v } + +func deref[T string | int | int32 | int64](v *T) T { + if v == nil { + var zero T + return zero + } + + return *v +} diff --git a/providers/dns/exoscale/exoscale.toml b/providers/dns/exoscale/exoscale.toml index bcc912b07..182a66ced 100644 --- a/providers/dns/exoscale/exoscale.toml +++ b/providers/dns/exoscale/exoscale.toml @@ -7,7 +7,7 @@ Since = "v0.4.0" Example = ''' EXOSCALE_API_KEY=abcdefghijklmnopqrstuvwx \ EXOSCALE_API_SECRET=xxxxxxx \ -lego --dns exoscale -d '*.example.com' -d example.com run +lego --email you@example.com --dns exoscale --domains my.example.org run ''' [Configuration] @@ -16,10 +16,11 @@ lego --dns exoscale -d '*.example.com' -d example.com run EXOSCALE_API_SECRET = "API secret" [Configuration.Additional] EXOSCALE_ENDPOINT = "API endpoint URL" - EXOSCALE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - EXOSCALE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - EXOSCALE_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - EXOSCALE_HTTP_TIMEOUT = "API request timeout in seconds (Default: 60)" + EXOSCALE_API_ZONE = "API zone" + EXOSCALE_POLLING_INTERVAL = "Time between DNS propagation check" + EXOSCALE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + EXOSCALE_TTL = "The TTL of the TXT record used for the DNS challenge" + EXOSCALE_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://openapi-v2.exoscale.com/#endpoint-dns" diff --git a/providers/dns/exoscale/exoscale_test.go b/providers/dns/exoscale/exoscale_test.go index e9f6be602..fa58216a5 100644 --- a/providers/dns/exoscale/exoscale_test.go +++ b/providers/dns/exoscale/exoscale_test.go @@ -58,7 +58,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -179,7 +178,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -197,7 +195,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/f5xc/f5xc.go b/providers/dns/f5xc/f5xc.go deleted file mode 100644 index 76a6e0262..000000000 --- a/providers/dns/f5xc/f5xc.go +++ /dev/null @@ -1,201 +0,0 @@ -// Package f5xc implements a DNS provider for solving the DNS-01 challenge using F5 XC. -package f5xc - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/cenkalti/backoff/v5" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/platform/wait" - "github.com/go-acme/lego/v4/providers/dns/f5xc/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "F5XC_" - - EnvToken = envNamespace + "API_TOKEN" - EnvTenantName = envNamespace + "TENANT_NAME" - EnvServer = envNamespace + "SERVER" - EnvGroupName = envNamespace + "GROUP_NAME" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIToken string - TenantName string - Server string - GroupName string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for F5 XC. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvToken, EnvTenantName, EnvGroupName) - if err != nil { - return nil, fmt.Errorf("f5xc: %w", err) - } - - config := NewDefaultConfig() - config.APIToken = values[EnvToken] - config.TenantName = values[EnvTenantName] - config.GroupName = values[EnvGroupName] - config.Server = env.GetOrFile(EnvServer) - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for F5 XC. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("f5xc: the configuration of the DNS provider is nil") - } - - if config.GroupName == "" { - return nil, errors.New("f5xc: missing group name") - } - - client, err := internal.NewClient(config.APIToken, config.TenantName, config.Server) - if err != nil { - return nil, fmt.Errorf("f5xc: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("f5xc: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("f5xc: %w", err) - } - - existingRRSet, err := d.client.GetRRSet(ctx, dns01.UnFqdn(authZone), d.config.GroupName, subDomain, "TXT") - if err != nil { - return fmt.Errorf("f5xc: get RR Set: %w", err) - } - - // New RRSet. - if existingRRSet == nil || existingRRSet.RRSet.TXTRecord == nil { - rrSet := internal.RRSet{ - Description: "lego", - TTL: d.config.TTL, - TXTRecord: &internal.TXTRecord{ - Name: subDomain, - Values: []string{info.Value}, - }, - } - - return d.waitFor(ctx, func() error { - _, err = d.client.CreateRRSet(ctx, dns01.UnFqdn(authZone), d.config.GroupName, rrSet) - if err != nil { - return fmt.Errorf("create RR set: %w", err) - } - - return nil - }) - } - - // Update RRSet. - existingRRSet.RRSet.TXTRecord.Values = append(existingRRSet.RRSet.TXTRecord.Values, info.Value) - - return d.waitFor(ctx, func() error { - _, err = d.client.ReplaceRRSet(ctx, dns01.UnFqdn(authZone), d.config.GroupName, subDomain, "TXT", existingRRSet.RRSet) - if err != nil { - return fmt.Errorf("replace RR set: %w", err) - } - - return nil - }) -} - -func (d *DNSProvider) waitFor(ctx context.Context, operation func() error) error { - err := wait.Retry(ctx, operation, - backoff.WithBackOff(backoff.NewConstantBackOff(2*time.Second)), - backoff.WithMaxElapsedTime(60*time.Second), - ) - if err != nil { - return fmt.Errorf("f5xc: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("f5xc: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("f5xc: %w", err) - } - - _, err = d.client.DeleteRRSet(context.Background(), dns01.UnFqdn(authZone), d.config.GroupName, subDomain, "TXT") - if err != nil { - return fmt.Errorf("f5xc: delete RR set: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/f5xc/f5xc.toml b/providers/dns/f5xc/f5xc.toml deleted file mode 100644 index 6be604ddd..000000000 --- a/providers/dns/f5xc/f5xc.toml +++ /dev/null @@ -1,28 +0,0 @@ -Name = "F5 XC" -Description = '''''' -URL = "https://www.f5.com/products/distributed-cloud-services" -Code = "f5xc" -Since = "v4.23.0" - -Example = ''' -F5XC_API_TOKEN="xxx" \ -F5XC_TENANT_NAME="yyy" \ -F5XC_GROUP_NAME="zzz" \ -lego --dns f5xc -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - F5XC_API_TOKEN = "API token" - F5XC_TENANT_NAME = "XC Tenant shortname" - F5XC_GROUP_NAME = "Group name" - [Configuration.Additional] - F5XC_SERVER = "Server domain (Default: console.ves.volterra.io)" - F5XC_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - F5XC_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - F5XC_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - F5XC_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://docs.cloud.f5.com/docs-v2/api/dns-zone-rrset" - Documentation = "https://my.f5.com/manage/s/article/K000147937" diff --git a/providers/dns/f5xc/f5xc_test.go b/providers/dns/f5xc/f5xc_test.go deleted file mode 100644 index 890a4cf09..000000000 --- a/providers/dns/f5xc/f5xc_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package f5xc - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest( - EnvToken, - EnvTenantName, - EnvServer, - EnvGroupName, -).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvToken: "secret", - EnvTenantName: "shortname", - EnvGroupName: "group", - }, - }, - { - desc: "missing API token", - envVars: map[string]string{ - EnvToken: "", - EnvTenantName: "shortname", - EnvGroupName: "group", - }, - expected: "f5xc: some credentials information are missing: F5XC_API_TOKEN", - }, - { - desc: "missing tenant name", - envVars: map[string]string{ - EnvToken: "secret", - EnvTenantName: "", - EnvGroupName: "group", - }, - expected: "f5xc: some credentials information are missing: F5XC_TENANT_NAME", - }, - { - desc: "missing group name", - envVars: map[string]string{ - EnvToken: "secret", - EnvTenantName: "shortname", - EnvGroupName: "", - }, - expected: "f5xc: some credentials information are missing: F5XC_GROUP_NAME", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "f5xc: some credentials information are missing: F5XC_API_TOKEN,F5XC_TENANT_NAME,F5XC_GROUP_NAME", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiToken string - tenantName string - groupName string - expected string - }{ - { - desc: "success", - apiToken: "secret", - tenantName: "shortname", - groupName: "group", - }, - { - desc: "missing API token", - tenantName: "shortname", - groupName: "group", - expected: "f5xc: credentials missing", - }, - { - desc: "missing tenant name", - apiToken: "secret", - groupName: "group", - expected: "f5xc: missing tenant name", - }, - { - desc: "missing group name", - apiToken: "secret", - tenantName: "shortname", - expected: "f5xc: missing group name", - }, - { - desc: "missing credentials", - expected: "f5xc: missing group name", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIToken = test.apiToken - config.TenantName = test.tenantName - config.GroupName = test.groupName - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/f5xc/internal/client.go b/providers/dns/f5xc/internal/client.go deleted file mode 100644 index 7beab0d03..000000000 --- a/providers/dns/f5xc/internal/client.go +++ /dev/null @@ -1,224 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" -) - -const defaultServer = "console.ves.volterra.io" - -const authorizationHeader = "Authorization" - -// Client the F5 XC API client. -type Client struct { - apiToken string - - baseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(apiToken, tenantName, server string) (*Client, error) { - if apiToken == "" { - return nil, errors.New("credentials missing") - } - - baseURL, err := createBaseURL(tenantName, server) - if err != nil { - return nil, err - } - - return &Client{ - apiToken: apiToken, - baseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -// CreateRRSet creates RRSet. -// https://docs.cloud.f5.com/docs-v2/api/dns-zone-rrset#operation/ves.io.schema.dns_zone.rrset.CustomAPI.Create -func (c *Client) CreateRRSet(ctx context.Context, dnsZoneName, groupName string, rrSet RRSet) (*APIRRSet, error) { - endpoint := c.baseURL.JoinPath("api", "config", "dns", "namespaces", "system", "dns_zones", dnsZoneName, "rrsets", groupName) - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, APIRRSet{ - DNSZoneName: dnsZoneName, - GroupName: groupName, - RRSet: rrSet, - }) - if err != nil { - return nil, err - } - - result := &APIRRSet{} - - err = c.do(req, result) - if err != nil { - return nil, err - } - - return result, nil -} - -// GetRRSet gets RRSets. -// https://docs.cloud.f5.com/docs-v2/api/dns-zone-rrset#operation/ves.io.schema.dns_zone.rrset.CustomAPI.Get -func (c *Client) GetRRSet(ctx context.Context, dnsZoneName, groupName, recordName, recordType string) (*APIRRSet, error) { - endpoint := c.baseURL.JoinPath("api", "config", "dns", "namespaces", "system", "dns_zones", dnsZoneName, "rrsets", groupName, recordName, recordType) - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - result := &APIRRSet{} - - err = c.do(req, result) - if err != nil { - usce := &APIError{} - if errors.As(err, &usce) && usce.StatusCode == http.StatusNotFound { - return nil, nil - } - - return nil, err - } - - return result, nil -} - -// DeleteRRSet deletes RRSet. -// https://docs.cloud.f5.com/docs-v2/api/dns-zone-rrset#operation/ves.io.schema.dns_zone.rrset.CustomAPI.Delete -func (c *Client) DeleteRRSet(ctx context.Context, dnsZoneName, groupName, recordName, recordType string) (*APIRRSet, error) { - endpoint := c.baseURL.JoinPath("api", "config", "dns", "namespaces", "system", "dns_zones", dnsZoneName, "rrsets", groupName, recordName, recordType) - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return nil, err - } - - result := &APIRRSet{} - - err = c.do(req, result) - if err != nil { - return nil, err - } - - return result, nil -} - -// ReplaceRRSet replaces RRSet. -// https://docs.cloud.f5.com/docs-v2/api/dns-zone-rrset#operation/ves.io.schema.dns_zone.rrset.CustomAPI.Replace -func (c *Client) ReplaceRRSet(ctx context.Context, dnsZoneName, groupName, recordName, recordType string, rrSet RRSet) (*APIRRSet, error) { - endpoint := c.baseURL.JoinPath("api", "config", "dns", "namespaces", "system", "dns_zones", dnsZoneName, "rrsets", groupName, recordName, recordType) - - req, err := newJSONRequest(ctx, http.MethodPut, endpoint, APIRRSet{ - DNSZoneName: dnsZoneName, - GroupName: groupName, - RRSet: rrSet, - Type: recordType, - }) - if err != nil { - return nil, err - } - - result := &APIRRSet{} - - err = c.do(req, result) - if err != nil { - return nil, err - } - - return result, nil -} - -func (c *Client) do(req *http.Request, result any) error { - req.Header.Set(authorizationHeader, "APIToken "+c.apiToken) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - apiErr := APIError{StatusCode: resp.StatusCode} - - err := json.Unmarshal(raw, &apiErr) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return &apiErr -} - -func createBaseURL(tenant, server string) (*url.URL, error) { - if tenant == "" { - return nil, errors.New("missing tenant name") - } - - if server == "" { - server = defaultServer - } - - baseURL, err := url.Parse(fmt.Sprintf("https://%s.%s", tenant, server)) - if err != nil { - return nil, fmt.Errorf("parse base URL: %w", err) - } - - return baseURL, nil -} diff --git a/providers/dns/f5xc/internal/client_test.go b/providers/dns/f5xc/internal/client_test.go deleted file mode 100644 index bb188ef3f..000000000 --- a/providers/dns/f5xc/internal/client_test.go +++ /dev/null @@ -1,291 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("secret", "shortname", "") - if err != nil { - return nil, err - } - - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("APIToken secret")) -} - -func TestClient_CreateRRSet(t *testing.T) { - client := mockBuilder(). - Route("POST /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA", - servermock.ResponseFromFixture("create.json"), - servermock.CheckRequestJSONBody(`{"dns_zone_name":"example.com","group_name":"groupA","rrset":{"description":"lego","ttl":60,"txt_record":{"name":"wwww","values":["txt"]}}}`)). - Build(t) - - rrSet := RRSet{ - Description: "lego", - TTL: 60, - TXTRecord: &TXTRecord{ - Name: "wwww", - Values: []string{"txt"}, - }, - } - - result, err := client.CreateRRSet(t.Context(), "example.com", "groupA", rrSet) - require.NoError(t, err) - - expected := &APIRRSet{ - DNSZoneName: "string", - GroupName: "string", - RRSet: RRSet{ - Description: "string", - TXTRecord: &TXTRecord{ - Name: "string", - Values: []string{"string"}, - }, - }, - } - - assert.Equal(t, expected, result) -} - -func TestClient_CreateRRSet_error(t *testing.T) { - client := mockBuilder(). - Route("POST /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA", - servermock.Noop().WithStatusCode(http.StatusBadRequest)). - Build(t) - - rrSet := RRSet{ - Description: "lego", - TTL: 60, - TXTRecord: &TXTRecord{ - Name: "wwww", - Values: []string{"txt"}, - }, - } - - _, err := client.CreateRRSet(t.Context(), "example.com", "groupA", rrSet) - require.Error(t, err) -} - -func TestClient_GetRRSet(t *testing.T) { - client := mockBuilder(). - Route("GET /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", - servermock.ResponseFromFixture("get.json")). - Build(t) - - result, err := client.GetRRSet(t.Context(), "example.com", "groupA", "www", "TXT") - require.NoError(t, err) - - expected := &APIRRSet{ - DNSZoneName: "string", - GroupName: "string", - Namespace: "string", - RecordName: "string", - Type: "string", - RRSet: RRSet{ - Description: "string", - TXTRecord: &TXTRecord{ - Name: "string", - Values: []string{"string"}, - }, - }, - } - - assert.Equal(t, expected, result) -} - -func TestClient_GetRRSet_not_found(t *testing.T) { - client := mockBuilder(). - Route("GET /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", - servermock.ResponseFromFixture("error_404.json").WithStatusCode(http.StatusNotFound)). - Build(t) - - result, err := client.GetRRSet(t.Context(), "example.com", "groupA", "www", "TXT") - require.NoError(t, err) - - assert.Nil(t, result) -} - -func TestClient_GetRRSet_error(t *testing.T) { - client := mockBuilder(). - Route("GET /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", - servermock.Noop().WithStatusCode(http.StatusBadRequest)). - Build(t) - - _, err := client.GetRRSet(t.Context(), "example.com", "groupA", "www", "TXT") - require.Error(t, err) -} - -func TestClient_DeleteRRSet(t *testing.T) { - client := mockBuilder(). - Route("DELETE /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", - servermock.ResponseFromFixture("get.json")). - Build(t) - - result, err := client.DeleteRRSet(t.Context(), "example.com", "groupA", "www", "TXT") - require.NoError(t, err) - - expected := &APIRRSet{ - DNSZoneName: "string", - GroupName: "string", - Namespace: "string", - RecordName: "string", - Type: "string", - RRSet: RRSet{ - Description: "string", - TXTRecord: &TXTRecord{ - Name: "string", - Values: []string{"string"}, - }, - }, - } - - assert.Equal(t, expected, result) -} - -func TestClient_DeleteRRSet_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", - servermock.Noop().WithStatusCode(http.StatusBadRequest)). - Build(t) - - _, err := client.DeleteRRSet(t.Context(), "example.com", "groupA", "www", "TXT") - require.Error(t, err) -} - -func TestClient_ReplaceRRSet(t *testing.T) { - client := mockBuilder(). - Route("PUT /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", - servermock.ResponseFromFixture("get.json"), - servermock.CheckRequestJSONBody(`{"dns_zone_name":"example.com","group_name":"groupA","type":"TXT","rrset":{"description":"lego","ttl":60,"txt_record":{"name":"wwww","values":["txt"]}}}`)). - Build(t) - - rrSet := RRSet{ - Description: "lego", - TTL: 60, - TXTRecord: &TXTRecord{ - Name: "wwww", - Values: []string{"txt"}, - }, - } - - result, err := client.ReplaceRRSet(t.Context(), "example.com", "groupA", "www", "TXT", rrSet) - require.NoError(t, err) - - expected := &APIRRSet{ - DNSZoneName: "string", - GroupName: "string", - Namespace: "string", - RecordName: "string", - Type: "string", - RRSet: RRSet{ - Description: "string", - TXTRecord: &TXTRecord{ - Name: "string", - Values: []string{"string"}, - }, - }, - } - - assert.Equal(t, expected, result) -} - -func TestClient_ReplaceRRSet_error(t *testing.T) { - client := mockBuilder(). - Route("PUT /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", - servermock.Noop().WithStatusCode(http.StatusBadRequest)). - Build(t) - - rrSet := RRSet{ - Description: "lego", - TTL: 60, - TXTRecord: &TXTRecord{ - Name: "wwww", - Values: []string{"txt"}, - }, - } - - _, err := client.ReplaceRRSet(t.Context(), "example.com", "groupA", "www", "TXT", rrSet) - require.Error(t, err) -} - -func Test_createBaseURL(t *testing.T) { - testCases := []struct { - desc string - tenant string - server string - expected string - }{ - { - desc: "only tenant", - tenant: "foo", - expected: "https://foo.console.ves.volterra.io", - }, - { - desc: "custom server", - tenant: "foo", - server: "example.com", - expected: "https://foo.example.com", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - baseURL, err := createBaseURL(test.tenant, test.server) - require.NoError(t, err) - - assert.Equal(t, test.expected, baseURL.String()) - }) - } -} - -func Test_createBaseURL_error(t *testing.T) { - testCases := []struct { - desc string - tenant string - server string - expected string - }{ - { - desc: "no tenant", - tenant: "", - expected: "missing tenant name", - }, - { - desc: "invalid tenant", - tenant: "%31", - expected: `parse base URL: parse "https://%31.console.ves.volterra.io": invalid URL escape "%31"`, - }, - { - desc: "invalid host", - tenant: "foo", - server: "192.168.0.%31", - expected: `parse base URL: parse "https://foo.192.168.0.%31": invalid URL escape "%31"`, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - _, err := createBaseURL(test.tenant, test.server) - require.EqualError(t, err, test.expected) - }) - } -} diff --git a/providers/dns/f5xc/internal/fixtures/create.json b/providers/dns/f5xc/internal/fixtures/create.json deleted file mode 100644 index 8c852304d..000000000 --- a/providers/dns/f5xc/internal/fixtures/create.json +++ /dev/null @@ -1,204 +0,0 @@ -{ - "dns_zone_name": "string", - "group_name": "string", - "rrset": { - "a_record": { - "name": "string", - "values": [ - "string" - ] - }, - "aaaa_record": { - "name": "string", - "values": [ - "string" - ] - }, - "afsdb_record": { - "name": "string", - "values": [ - { - "hostname": "string", - "subtype": "NONE" - } - ] - }, - "alias_record": { - "value": "string" - }, - "caa_record": { - "name": "string", - "values": [ - { - "flags": 0, - "tag": "string", - "value": "string" - } - ] - }, - "cds_record": { - "name": "string", - "values": [ - { - "ds_key_algorithm": "UNSPECIFIED", - "key_tag": 0, - "sha1_digest": { - "digest": "stringstringstringstringstringstringstri" - }, - "sha256_digest": { - "digest": "stringstringstringstringstringstringstringstringstringstringstri" - }, - "sha384_digest": { - "digest": "stringstringstringstringstringstringstringstringstringstringstringstringstringstringstringstring" - } - } - ] - }, - "cert_record": { - "name": "string", - "values": [ - { - "algorithm": "RESERVEDALGORITHM", - "cert_key_tag": 0, - "cert_type": "INVALIDCERTTYPE", - "certificate": "string" - } - ] - }, - "cname_record": { - "name": "string", - "value": "string" - }, - "description": "string", - "ds_record": { - "name": "string", - "values": [ - { - "ds_key_algorithm": "UNSPECIFIED", - "key_tag": 0, - "sha1_digest": { - "digest": "stringstringstringstringstringstringstri" - }, - "sha256_digest": { - "digest": "stringstringstringstringstringstringstringstringstringstringstri" - }, - "sha384_digest": { - "digest": "stringstringstringstringstringstringstringstringstringstringstringstringstringstringstringstring" - } - } - ] - }, - "eui48_record": { - "name": "string", - "value": "stringstringstrin" - }, - "eui64_record": { - "name": "string", - "value": "stringstringstringstrin" - }, - "lb_record": { - "name": "string", - "value": { - "name": "string", - "namespace": "string", - "tenant": "string" - } - }, - "loc_record": { - "name": "string", - "values": [ - { - "altitude": 0.1, - "horizontal_precision": 0.1, - "latitude_degree": 0, - "latitude_hemisphere": "N", - "latitude_minute": 0, - "latitude_second": 0.1, - "location_diameter": 0.1, - "longitude_degree": 0, - "longitude_hemisphere": "E", - "longitude_minute": 0, - "longitude_second": 0.1, - "vertical_precision": 0.1 - } - ] - }, - "mx_record": { - "name": "string", - "values": [ - { - "domain": "string", - "priority": 0 - } - ] - }, - "naptr_record": { - "name": "string", - "values": [ - { - "flags": "string", - "order": 0, - "preference": 0, - "regexp": "string", - "replacement": "string", - "service": "string" - } - ] - }, - "ns_record": { - "name": "string", - "values": [ - "string" - ] - }, - "ptr_record": { - "name": "string", - "values": [ - "string" - ] - }, - "srv_record": { - "name": "string", - "values": [ - { - "port": 0, - "priority": 0, - "target": "string", - "weight": 0 - } - ] - }, - "sshfp_record": { - "name": "string", - "values": [ - { - "algorithm": "UNSPECIFIEDALGORITHM", - "sha1_fingerprint": { - "fingerprint": "stringstringstringstringstringstringstri" - }, - "sha256_fingerprint": { - "fingerprint": "stringstringstringstringstringstringstringstringstringstringstri" - } - } - ] - }, - "tlsa_record": { - "name": "string", - "values": [ - { - "certificate_association_data": "string", - "certificate_usage": "CertificateAuthorityConstraint", - "matching_type": "NoHash", - "selector": "FullCertificate" - } - ] - }, - "ttl": 0, - "txt_record": { - "name": "string", - "values": [ - "string" - ] - } - } -} diff --git a/providers/dns/f5xc/internal/fixtures/delete.json b/providers/dns/f5xc/internal/fixtures/delete.json deleted file mode 100644 index 5c5143cae..000000000 --- a/providers/dns/f5xc/internal/fixtures/delete.json +++ /dev/null @@ -1,207 +0,0 @@ -{ - "dns_zone_name": "string", - "group_name": "string", - "namespace": "string", - "record_name": "string", - "rrset": { - "a_record": { - "name": "string", - "values": [ - "string" - ] - }, - "aaaa_record": { - "name": "string", - "values": [ - "string" - ] - }, - "afsdb_record": { - "name": "string", - "values": [ - { - "hostname": "string", - "subtype": "NONE" - } - ] - }, - "alias_record": { - "value": "string" - }, - "caa_record": { - "name": "string", - "values": [ - { - "flags": 0, - "tag": "string", - "value": "string" - } - ] - }, - "cds_record": { - "name": "string", - "values": [ - { - "ds_key_algorithm": "UNSPECIFIED", - "key_tag": 0, - "sha1_digest": { - "digest": "stringstringstringstringstringstringstri" - }, - "sha256_digest": { - "digest": "stringstringstringstringstringstringstringstringstringstringstri" - }, - "sha384_digest": { - "digest": "stringstringstringstringstringstringstringstringstringstringstringstringstringstringstringstring" - } - } - ] - }, - "cert_record": { - "name": "string", - "values": [ - { - "algorithm": "RESERVEDALGORITHM", - "cert_key_tag": 0, - "cert_type": "INVALIDCERTTYPE", - "certificate": "string" - } - ] - }, - "cname_record": { - "name": "string", - "value": "string" - }, - "description": "string", - "ds_record": { - "name": "string", - "values": [ - { - "ds_key_algorithm": "UNSPECIFIED", - "key_tag": 0, - "sha1_digest": { - "digest": "stringstringstringstringstringstringstri" - }, - "sha256_digest": { - "digest": "stringstringstringstringstringstringstringstringstringstringstri" - }, - "sha384_digest": { - "digest": "stringstringstringstringstringstringstringstringstringstringstringstringstringstringstringstring" - } - } - ] - }, - "eui48_record": { - "name": "string", - "value": "stringstringstrin" - }, - "eui64_record": { - "name": "string", - "value": "stringstringstringstrin" - }, - "lb_record": { - "name": "string", - "value": { - "name": "string", - "namespace": "string", - "tenant": "string" - } - }, - "loc_record": { - "name": "string", - "values": [ - { - "altitude": 0.1, - "horizontal_precision": 0.1, - "latitude_degree": 0, - "latitude_hemisphere": "N", - "latitude_minute": 0, - "latitude_second": 0.1, - "location_diameter": 0.1, - "longitude_degree": 0, - "longitude_hemisphere": "E", - "longitude_minute": 0, - "longitude_second": 0.1, - "vertical_precision": 0.1 - } - ] - }, - "mx_record": { - "name": "string", - "values": [ - { - "domain": "string", - "priority": 0 - } - ] - }, - "naptr_record": { - "name": "string", - "values": [ - { - "flags": "string", - "order": 0, - "preference": 0, - "regexp": "string", - "replacement": "string", - "service": "string" - } - ] - }, - "ns_record": { - "name": "string", - "values": [ - "string" - ] - }, - "ptr_record": { - "name": "string", - "values": [ - "string" - ] - }, - "srv_record": { - "name": "string", - "values": [ - { - "port": 0, - "priority": 0, - "target": "string", - "weight": 0 - } - ] - }, - "sshfp_record": { - "name": "string", - "values": [ - { - "algorithm": "UNSPECIFIEDALGORITHM", - "sha1_fingerprint": { - "fingerprint": "stringstringstringstringstringstringstri" - }, - "sha256_fingerprint": { - "fingerprint": "stringstringstringstringstringstringstringstringstringstringstri" - } - } - ] - }, - "tlsa_record": { - "name": "string", - "values": [ - { - "certificate_association_data": "string", - "certificate_usage": "CertificateAuthorityConstraint", - "matching_type": "NoHash", - "selector": "FullCertificate" - } - ] - }, - "ttl": 0, - "txt_record": { - "name": "string", - "values": [ - "string" - ] - } - }, - "type": "string" -} diff --git a/providers/dns/f5xc/internal/fixtures/error_404.json b/providers/dns/f5xc/internal/fixtures/error_404.json deleted file mode 100644 index 4abd79dd4..000000000 --- a/providers/dns/f5xc/internal/fixtures/error_404.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": 5, - "details": [], - "message": "the requested resource record was not found: (group,name,type) (acme-records,_acme-challenge,TXT)" -} diff --git a/providers/dns/f5xc/internal/fixtures/error_503.json b/providers/dns/f5xc/internal/fixtures/error_503.json deleted file mode 100644 index 8d286a2a0..000000000 --- a/providers/dns/f5xc/internal/fixtures/error_503.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": 14, - "details": [], - "message": "Previous DNS zone change is pending. Try again later" -} diff --git a/providers/dns/f5xc/internal/fixtures/get.json b/providers/dns/f5xc/internal/fixtures/get.json deleted file mode 100644 index 5c5143cae..000000000 --- a/providers/dns/f5xc/internal/fixtures/get.json +++ /dev/null @@ -1,207 +0,0 @@ -{ - "dns_zone_name": "string", - "group_name": "string", - "namespace": "string", - "record_name": "string", - "rrset": { - "a_record": { - "name": "string", - "values": [ - "string" - ] - }, - "aaaa_record": { - "name": "string", - "values": [ - "string" - ] - }, - "afsdb_record": { - "name": "string", - "values": [ - { - "hostname": "string", - "subtype": "NONE" - } - ] - }, - "alias_record": { - "value": "string" - }, - "caa_record": { - "name": "string", - "values": [ - { - "flags": 0, - "tag": "string", - "value": "string" - } - ] - }, - "cds_record": { - "name": "string", - "values": [ - { - "ds_key_algorithm": "UNSPECIFIED", - "key_tag": 0, - "sha1_digest": { - "digest": "stringstringstringstringstringstringstri" - }, - "sha256_digest": { - "digest": "stringstringstringstringstringstringstringstringstringstringstri" - }, - "sha384_digest": { - "digest": "stringstringstringstringstringstringstringstringstringstringstringstringstringstringstringstring" - } - } - ] - }, - "cert_record": { - "name": "string", - "values": [ - { - "algorithm": "RESERVEDALGORITHM", - "cert_key_tag": 0, - "cert_type": "INVALIDCERTTYPE", - "certificate": "string" - } - ] - }, - "cname_record": { - "name": "string", - "value": "string" - }, - "description": "string", - "ds_record": { - "name": "string", - "values": [ - { - "ds_key_algorithm": "UNSPECIFIED", - "key_tag": 0, - "sha1_digest": { - "digest": "stringstringstringstringstringstringstri" - }, - "sha256_digest": { - "digest": "stringstringstringstringstringstringstringstringstringstringstri" - }, - "sha384_digest": { - "digest": "stringstringstringstringstringstringstringstringstringstringstringstringstringstringstringstring" - } - } - ] - }, - "eui48_record": { - "name": "string", - "value": "stringstringstrin" - }, - "eui64_record": { - "name": "string", - "value": "stringstringstringstrin" - }, - "lb_record": { - "name": "string", - "value": { - "name": "string", - "namespace": "string", - "tenant": "string" - } - }, - "loc_record": { - "name": "string", - "values": [ - { - "altitude": 0.1, - "horizontal_precision": 0.1, - "latitude_degree": 0, - "latitude_hemisphere": "N", - "latitude_minute": 0, - "latitude_second": 0.1, - "location_diameter": 0.1, - "longitude_degree": 0, - "longitude_hemisphere": "E", - "longitude_minute": 0, - "longitude_second": 0.1, - "vertical_precision": 0.1 - } - ] - }, - "mx_record": { - "name": "string", - "values": [ - { - "domain": "string", - "priority": 0 - } - ] - }, - "naptr_record": { - "name": "string", - "values": [ - { - "flags": "string", - "order": 0, - "preference": 0, - "regexp": "string", - "replacement": "string", - "service": "string" - } - ] - }, - "ns_record": { - "name": "string", - "values": [ - "string" - ] - }, - "ptr_record": { - "name": "string", - "values": [ - "string" - ] - }, - "srv_record": { - "name": "string", - "values": [ - { - "port": 0, - "priority": 0, - "target": "string", - "weight": 0 - } - ] - }, - "sshfp_record": { - "name": "string", - "values": [ - { - "algorithm": "UNSPECIFIEDALGORITHM", - "sha1_fingerprint": { - "fingerprint": "stringstringstringstringstringstringstri" - }, - "sha256_fingerprint": { - "fingerprint": "stringstringstringstringstringstringstringstringstringstringstri" - } - } - ] - }, - "tlsa_record": { - "name": "string", - "values": [ - { - "certificate_association_data": "string", - "certificate_usage": "CertificateAuthorityConstraint", - "matching_type": "NoHash", - "selector": "FullCertificate" - } - ] - }, - "ttl": 0, - "txt_record": { - "name": "string", - "values": [ - "string" - ] - } - }, - "type": "string" -} diff --git a/providers/dns/f5xc/internal/fixtures/replace.json b/providers/dns/f5xc/internal/fixtures/replace.json deleted file mode 100644 index e3e483df5..000000000 --- a/providers/dns/f5xc/internal/fixtures/replace.json +++ /dev/null @@ -1,206 +0,0 @@ -{ - "dns_zone_name": "string", - "group_name": "string", - "record_name": "string", - "rrset": { - "a_record": { - "name": "string", - "values": [ - "string" - ] - }, - "aaaa_record": { - "name": "string", - "values": [ - "string" - ] - }, - "afsdb_record": { - "name": "string", - "values": [ - { - "hostname": "string", - "subtype": "NONE" - } - ] - }, - "alias_record": { - "value": "string" - }, - "caa_record": { - "name": "string", - "values": [ - { - "flags": 0, - "tag": "string", - "value": "string" - } - ] - }, - "cds_record": { - "name": "string", - "values": [ - { - "ds_key_algorithm": "UNSPECIFIED", - "key_tag": 0, - "sha1_digest": { - "digest": "stringstringstringstringstringstringstri" - }, - "sha256_digest": { - "digest": "stringstringstringstringstringstringstringstringstringstringstri" - }, - "sha384_digest": { - "digest": "stringstringstringstringstringstringstringstringstringstringstringstringstringstringstringstring" - } - } - ] - }, - "cert_record": { - "name": "string", - "values": [ - { - "algorithm": "RESERVEDALGORITHM", - "cert_key_tag": 0, - "cert_type": "INVALIDCERTTYPE", - "certificate": "string" - } - ] - }, - "cname_record": { - "name": "string", - "value": "string" - }, - "description": "string", - "ds_record": { - "name": "string", - "values": [ - { - "ds_key_algorithm": "UNSPECIFIED", - "key_tag": 0, - "sha1_digest": { - "digest": "stringstringstringstringstringstringstri" - }, - "sha256_digest": { - "digest": "stringstringstringstringstringstringstringstringstringstringstri" - }, - "sha384_digest": { - "digest": "stringstringstringstringstringstringstringstringstringstringstringstringstringstringstringstring" - } - } - ] - }, - "eui48_record": { - "name": "string", - "value": "stringstringstrin" - }, - "eui64_record": { - "name": "string", - "value": "stringstringstringstrin" - }, - "lb_record": { - "name": "string", - "value": { - "name": "string", - "namespace": "string", - "tenant": "string" - } - }, - "loc_record": { - "name": "string", - "values": [ - { - "altitude": 0.1, - "horizontal_precision": 0.1, - "latitude_degree": 0, - "latitude_hemisphere": "N", - "latitude_minute": 0, - "latitude_second": 0.1, - "location_diameter": 0.1, - "longitude_degree": 0, - "longitude_hemisphere": "E", - "longitude_minute": 0, - "longitude_second": 0.1, - "vertical_precision": 0.1 - } - ] - }, - "mx_record": { - "name": "string", - "values": [ - { - "domain": "string", - "priority": 0 - } - ] - }, - "naptr_record": { - "name": "string", - "values": [ - { - "flags": "string", - "order": 0, - "preference": 0, - "regexp": "string", - "replacement": "string", - "service": "string" - } - ] - }, - "ns_record": { - "name": "string", - "values": [ - "string" - ] - }, - "ptr_record": { - "name": "string", - "values": [ - "string" - ] - }, - "srv_record": { - "name": "string", - "values": [ - { - "port": 0, - "priority": 0, - "target": "string", - "weight": 0 - } - ] - }, - "sshfp_record": { - "name": "string", - "values": [ - { - "algorithm": "UNSPECIFIEDALGORITHM", - "sha1_fingerprint": { - "fingerprint": "stringstringstringstringstringstringstri" - }, - "sha256_fingerprint": { - "fingerprint": "stringstringstringstringstringstringstringstringstringstringstri" - } - } - ] - }, - "tlsa_record": { - "name": "string", - "values": [ - { - "certificate_association_data": "string", - "certificate_usage": "CertificateAuthorityConstraint", - "matching_type": "NoHash", - "selector": "FullCertificate" - } - ] - }, - "ttl": 0, - "txt_record": { - "name": "string", - "values": [ - "string" - ] - } - }, - "type": "string" -} diff --git a/providers/dns/f5xc/internal/types.go b/providers/dns/f5xc/internal/types.go deleted file mode 100644 index 346283fb7..000000000 --- a/providers/dns/f5xc/internal/types.go +++ /dev/null @@ -1,48 +0,0 @@ -package internal - -import ( - "fmt" - "strings" -) - -type APIError struct { - StatusCode int `json:"-"` - Code int `json:"code"` - Details []string `json:"details"` - Message string `json:"message"` -} - -func (a *APIError) Error() string { - var details string - if len(a.Details) > 0 { - details = " " + strings.Join(a.Details, ", ") - } - - return fmt.Sprintf("code: %d, message: %s%s", a.Code, a.Message, details) -} - -type APIRRSet struct { - DNSZoneName string `json:"dns_zone_name,omitempty"` - GroupName string `json:"group_name,omitempty"` - Namespace string `json:"namespace,omitempty"` - RecordName string `json:"record_name,omitempty"` - Type string `json:"type,omitempty"` - RRSet RRSet `json:"rrset"` -} - -type RRSetRequest struct { - DNSZoneName string `json:"dns_zone_name,omitempty"` - GroupName string `json:"group_name,omitempty"` - RRSet RRSet `json:"rrset"` -} - -type RRSet struct { - Description string `json:"description,omitempty"` - TTL int `json:"ttl,omitempty"` - TXTRecord *TXTRecord `json:"txt_record,omitempty"` -} - -type TXTRecord struct { - Name string `json:"name,omitempty"` - Values []string `json:"values,omitempty"` -} diff --git a/providers/dns/freemyip/freemyip.go b/providers/dns/freemyip/freemyip.go index fb6202e25..18fcb0565 100644 --- a/providers/dns/freemyip/freemyip.go +++ b/providers/dns/freemyip/freemyip.go @@ -8,10 +8,8 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/nrdcg/freemyip" ) @@ -28,8 +26,6 @@ const ( EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Token string @@ -89,8 +85,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, client: client, diff --git a/providers/dns/freemyip/freemyip.toml b/providers/dns/freemyip/freemyip.toml index adbf9e213..ff3b60142 100644 --- a/providers/dns/freemyip/freemyip.toml +++ b/providers/dns/freemyip/freemyip.toml @@ -6,18 +6,18 @@ Since = "v4.5.0" Example = ''' FREEMYIP_TOKEN=xxxxxx \ -lego --dns freemyip -d '*.example.com' -d example.com run +lego --email you@example.com --dns freemyip --domains my.example.org run ''' [Configuration] [Configuration.Credentials] FREEMYIP_TOKEN = "Account token" [Configuration.Additional] - FREEMYIP_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - FREEMYIP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - FREEMYIP_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)" - FREEMYIP_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - FREEMYIP_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 60)" + FREEMYIP_POLLING_INTERVAL = "Time between DNS propagation check" + FREEMYIP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + FREEMYIP_TTL = "The TTL of the TXT record used for the DNS challenge" + FREEMYIP_HTTP_TIMEOUT = "API request timeout" + FREEMYIP_SEQUENCE_INTERVAL = "Time between sequential requests" [Links] API = "https://freemyip.com/help" diff --git a/providers/dns/freemyip/freemyip_test.go b/providers/dns/freemyip/freemyip_test.go index 24d1b98f7..dcf74dd6c 100644 --- a/providers/dns/freemyip/freemyip_test.go +++ b/providers/dns/freemyip/freemyip_test.go @@ -37,7 +37,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -95,7 +94,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -109,7 +107,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/gandi/gandi.go b/providers/dns/gandi/gandi.go index bb96a7d0f..93e94f276 100644 --- a/providers/dns/gandi/gandi.go +++ b/providers/dns/gandi/gandi.go @@ -9,13 +9,16 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/gandi/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) +// Gandi API reference: http://doc.rpc.gandi.net/index.html +// Gandi API domain examples: http://doc.rpc.gandi.net/domain/faq.html + +const minTTL = 300 + // Environment variables names. const ( envNamespace = "GANDI_" @@ -28,10 +31,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const minTTL = 300 - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string @@ -110,8 +109,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, client: client, diff --git a/providers/dns/gandi/gandi.toml b/providers/dns/gandi/gandi.toml index 23d7de5db..0477bb7c7 100644 --- a/providers/dns/gandi/gandi.toml +++ b/providers/dns/gandi/gandi.toml @@ -6,17 +6,17 @@ Since = "v0.3.0" Example = ''' GANDI_API_KEY=abcdefghijklmnopqrstuvwx \ -lego --dns gandi -d '*.example.com' -d example.com run +lego --email you@example.com --dns gandi --domains my.example.org run ''' [Configuration] [Configuration.Credentials] GANDI_API_KEY = "API key" [Configuration.Additional] - GANDI_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 60)" - GANDI_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 2400)" - GANDI_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - GANDI_HTTP_TIMEOUT = "API request timeout in seconds (Default: 60)" + GANDI_POLLING_INTERVAL = "Time between DNS propagation check" + GANDI_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + GANDI_TTL = "The TTL of the TXT record used for the DNS challenge" + GANDI_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://doc.rpc.gandi.net/index.html" diff --git a/providers/dns/gandi/gandi_test.go b/providers/dns/gandi/gandi_test.go index 58c25d0db..36bc4ccd2 100644 --- a/providers/dns/gandi/gandi_test.go +++ b/providers/dns/gandi/gandi_test.go @@ -9,7 +9,6 @@ import ( "testing" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/require" ) @@ -39,7 +38,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -121,52 +119,47 @@ func TestDNSProvider(t *testing.T) { cleanupDeleteZoneRequestMock: cleanupDeleteZoneResponseMock, } + fakeKeyAuth := "XXXX" + regexpDate := regexp.MustCompile(`\[ACME Challenge [^\]:]*:[^\]]*\]`) - provider := servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.BaseURL = server.URL + "/" - config.HTTPClient = server.Client() - config.APIKey = "123412341234123412341234" + // start fake RPC server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "text/xml", r.Header.Get("Content-Type"), "invalid content type") - return NewDNSProviderConfig(config) - }, - servermock.CheckHeader().WithContentType("text/xml"), - ). - Route("POST /", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - require.Equal(t, "text/xml", req.Header.Get("Content-Type"), "invalid content type") + req, errS := io.ReadAll(r.Body) + require.NoError(t, errS) - body, errS := io.ReadAll(req.Body) - require.NoError(t, errS) + req = regexpDate.ReplaceAllLiteral(req, []byte(`[ACME Challenge 01 Jan 16 00:00 +0000]`)) + resp, ok := serverResponses[string(req)] + require.Truef(t, ok, "Server response for request not found: %s", string(req)) - body = regexpDate.ReplaceAllLiteral(body, []byte(`[ACME Challenge 01 Jan 16 00:00 +0000]`)) - resp, ok := serverResponses[string(body)] - require.Truef(t, ok, "Server response for request not found: %s", string(body)) - - _, errS = io.Copy(rw, strings.NewReader(resp)) - require.NoError(t, errS) - })). - Build(t) - - fakeKeyAuth := "XXXX" + _, errS = io.Copy(w, strings.NewReader(resp)) + require.NoError(t, errS) + })) + t.Cleanup(server.Close) // define function to override findZoneByFqdn with fakeFindZoneByFqdn := func(fqdn string) (string, error) { return "example.com.", nil } + config := NewDefaultConfig() + config.BaseURL = server.URL + "/" + config.APIKey = "123412341234123412341234" + + provider, err := NewDNSProviderConfig(config) + require.NoError(t, err) + // override findZoneByFqdn function savedFindZoneByFqdn := provider.findZoneByFqdn - t.Cleanup(func() { provider.findZoneByFqdn = savedFindZoneByFqdn }) - provider.findZoneByFqdn = fakeFindZoneByFqdn // run Present - err := provider.Present("abc.def.example.com", "", fakeKeyAuth) + err = provider.Present("abc.def.example.com", "", fakeKeyAuth) require.NoError(t, err) // run CleanUp diff --git a/providers/dns/gandi/internal/client.go b/providers/dns/gandi/internal/client.go index 6ca46d072..6dc09648c 100644 --- a/providers/dns/gandi/internal/client.go +++ b/providers/dns/gandi/internal/client.go @@ -50,7 +50,6 @@ func (c *Client) GetZoneID(ctx context.Context, domain string) (int, error) { } var zoneID int - for _, member := range resp.StructMembers { if member.Name == "zone_id" { zoneID = member.ValueInt @@ -60,7 +59,6 @@ func (c *Client) GetZoneID(ctx context.Context, domain string) (int, error) { if zoneID == 0 { return 0, fmt.Errorf("could not find zone_id for %s", domain) } - return zoneID, nil } @@ -90,7 +88,6 @@ func (c *Client) CloneZone(ctx context.Context, zoneID int, name string) (int, e } var newZoneID int - for _, member := range resp.StructMembers { if member.Name == "id" { newZoneID = member.ValueInt @@ -100,7 +97,6 @@ func (c *Client) CloneZone(ctx context.Context, zoneID int, name string) (int, e if newZoneID == 0 { return 0, errors.New("could not determine cloned zone_id") } - return newZoneID, nil } @@ -123,7 +119,6 @@ func (c *Client) NewZoneVersion(ctx context.Context, zoneID int) (int, error) { if resp.Value == 0 { return 0, errors.New("could not create new zone version") } - return resp.Value, nil } @@ -179,7 +174,6 @@ func (c *Client) SetZoneVersion(ctx context.Context, zoneID, version int) error if !resp.Value { return errors.New("could not set zone version") } - return nil } @@ -201,7 +195,6 @@ func (c *Client) SetZone(ctx context.Context, domain string, zoneID int) error { } var respZoneID int - for _, member := range resp.StructMembers { if member.Name == "zone_id" { respZoneID = member.ValueInt @@ -211,7 +204,6 @@ func (c *Client) SetZone(ctx context.Context, domain string, zoneID int) error { if respZoneID != zoneID { return fmt.Errorf("could not set new zone_id for %s", domain) } - return nil } diff --git a/providers/dns/gandi/internal/client_test.go b/providers/dns/gandi/internal/client_test.go deleted file mode 100644 index a800767a2..000000000 --- a/providers/dns/gandi/internal/client_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package internal - -import ( - "net/http/httptest" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("secret") - client.BaseURL = server.URL - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader().WithContentType("text/xml"), - ) -} - -func TestClient_GetZoneID(t *testing.T) { - client := mockBuilder(). - Route("POST /", servermock.ResponseFromFixture("get_zone_id.xml"), - servermock.CheckRequestBodyFromFixture("get_zone_id-request.xml").IgnoreWhitespace()). - Build(t) - - zoneID, err := client.GetZoneID(t.Context(), "example.com") - require.NoError(t, err) - - assert.Equal(t, 1, zoneID) -} - -func TestClient_CloneZone(t *testing.T) { - client := mockBuilder(). - Route("POST /", servermock.ResponseFromFixture("clone_zone.xml"), - servermock.CheckRequestBodyFromFixture("clone_zone-request.xml").IgnoreWhitespace()). - Build(t) - - zoneID, err := client.CloneZone(t.Context(), 6, "foo") - require.NoError(t, err) - - assert.Equal(t, 1, zoneID) -} - -func TestClient_NewZoneVersion(t *testing.T) { - client := mockBuilder(). - Route("POST /", servermock.ResponseFromFixture("new_zone_version.xml"), - servermock.CheckRequestBodyFromFixture("new_zone_version-request.xml").IgnoreWhitespace()). - Build(t) - - zoneID, err := client.NewZoneVersion(t.Context(), 6) - require.NoError(t, err) - - assert.Equal(t, 1, zoneID) -} - -func TestClient_AddTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /", servermock.ResponseFromFixture("empty.xml"), - servermock.CheckRequestBodyFromFixture("add_txt_record-request.xml").IgnoreWhitespace()). - Build(t) - - err := client.AddTXTRecord(t.Context(), 1, 123, "foo", "content", 120) - require.NoError(t, err) -} - -func TestClient_SetZoneVersion(t *testing.T) { - client := mockBuilder(). - Route("POST /", servermock.ResponseFromFixture("set_zone_version.xml"), - servermock.CheckRequestBodyFromFixture("set_zone_version-request.xml").IgnoreWhitespace()). - Build(t) - - err := client.SetZoneVersion(t.Context(), 1, 123) - require.NoError(t, err) -} - -func TestClient_SetZone(t *testing.T) { - client := mockBuilder(). - Route("POST /", servermock.ResponseFromFixture("set_zone.xml"), - servermock.CheckRequestBodyFromFixture("set_zone-request.xml").IgnoreWhitespace()). - Build(t) - - err := client.SetZone(t.Context(), "example.com", 1) - require.NoError(t, err) -} - -func TestClient_DeleteZone(t *testing.T) { - client := mockBuilder(). - Route("POST /", servermock.ResponseFromFixture("delete_zone.xml"), - servermock.CheckRequestBodyFromFixture("delete_zone-request.xml").IgnoreWhitespace()). - Build(t) - - err := client.DeleteZone(t.Context(), 1) - require.NoError(t, err) -} diff --git a/providers/dns/gandi/internal/fixtures/add_txt_record-request.xml b/providers/dns/gandi/internal/fixtures/add_txt_record-request.xml deleted file mode 100644 index 001ee7a33..000000000 --- a/providers/dns/gandi/internal/fixtures/add_txt_record-request.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - domain.zone.record.add - - - secret - - - - - 1 - - - - - 123 - - - - - - - type - - TXT - - - - name - - foo - - - - value - - content - - - - ttl - - 120 - - - - - - diff --git a/providers/dns/gandi/internal/fixtures/clone_zone-request.xml b/providers/dns/gandi/internal/fixtures/clone_zone-request.xml deleted file mode 100644 index 40ee87c7e..000000000 --- a/providers/dns/gandi/internal/fixtures/clone_zone-request.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - domain.zone.clone - - - secret - - - - - 6 - - - - - 0 - - - - - - - name - - foo - - - - - - diff --git a/providers/dns/gandi/internal/fixtures/clone_zone.xml b/providers/dns/gandi/internal/fixtures/clone_zone.xml deleted file mode 100644 index 2af93526e..000000000 --- a/providers/dns/gandi/internal/fixtures/clone_zone.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - id - - 1 - - - - foo - - 2 - - - - - - - diff --git a/providers/dns/gandi/internal/fixtures/delete_zone-request.xml b/providers/dns/gandi/internal/fixtures/delete_zone-request.xml deleted file mode 100644 index 0ba9cb766..000000000 --- a/providers/dns/gandi/internal/fixtures/delete_zone-request.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - domain.zone.delete - - - secret - - - - - 1 - - - diff --git a/providers/dns/gandi/internal/fixtures/delete_zone.xml b/providers/dns/gandi/internal/fixtures/delete_zone.xml deleted file mode 100644 index 28ba00dc5..000000000 --- a/providers/dns/gandi/internal/fixtures/delete_zone.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - true - - - - diff --git a/providers/dns/gandi/internal/fixtures/empty.xml b/providers/dns/gandi/internal/fixtures/empty.xml deleted file mode 100644 index 7843fd723..000000000 --- a/providers/dns/gandi/internal/fixtures/empty.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/providers/dns/gandi/internal/fixtures/get_zone_id-request.xml b/providers/dns/gandi/internal/fixtures/get_zone_id-request.xml deleted file mode 100644 index 173a725d8..000000000 --- a/providers/dns/gandi/internal/fixtures/get_zone_id-request.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - domain.info - - - secret - - - - - example.com - - - diff --git a/providers/dns/gandi/internal/fixtures/get_zone_id.xml b/providers/dns/gandi/internal/fixtures/get_zone_id.xml deleted file mode 100644 index 2a11e0dff..000000000 --- a/providers/dns/gandi/internal/fixtures/get_zone_id.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - zone_id - - 1 - - - - foo - - 2 - - - - - - - diff --git a/providers/dns/gandi/internal/fixtures/new_zone_version-request.xml b/providers/dns/gandi/internal/fixtures/new_zone_version-request.xml deleted file mode 100644 index 2fbac82de..000000000 --- a/providers/dns/gandi/internal/fixtures/new_zone_version-request.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - domain.zone.version.new - - - secret - - - - - 6 - - - diff --git a/providers/dns/gandi/internal/fixtures/new_zone_version.xml b/providers/dns/gandi/internal/fixtures/new_zone_version.xml deleted file mode 100644 index feb84e486..000000000 --- a/providers/dns/gandi/internal/fixtures/new_zone_version.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - 1 - - - - diff --git a/providers/dns/gandi/internal/fixtures/set_zone-request.xml b/providers/dns/gandi/internal/fixtures/set_zone-request.xml deleted file mode 100644 index 71ac843fd..000000000 --- a/providers/dns/gandi/internal/fixtures/set_zone-request.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - domain.zone.set - - - secret - - - - - example.com - - - - - 1 - - - diff --git a/providers/dns/gandi/internal/fixtures/set_zone.xml b/providers/dns/gandi/internal/fixtures/set_zone.xml deleted file mode 100644 index 2a11e0dff..000000000 --- a/providers/dns/gandi/internal/fixtures/set_zone.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - zone_id - - 1 - - - - foo - - 2 - - - - - - - diff --git a/providers/dns/gandi/internal/fixtures/set_zone_version-request.xml b/providers/dns/gandi/internal/fixtures/set_zone_version-request.xml deleted file mode 100644 index 68a021446..000000000 --- a/providers/dns/gandi/internal/fixtures/set_zone_version-request.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - domain.zone.version.set - - - secret - - - - - 1 - - - - - 123 - - - diff --git a/providers/dns/gandi/internal/fixtures/set_zone_version.xml b/providers/dns/gandi/internal/fixtures/set_zone_version.xml deleted file mode 100644 index 28ba00dc5..000000000 --- a/providers/dns/gandi/internal/fixtures/set_zone_version.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - true - - - - diff --git a/providers/dns/gandi/internal/types.go b/providers/dns/gandi/internal/types.go index 2cde62b53..cdcd0a658 100644 --- a/providers/dns/gandi/internal/types.go +++ b/providers/dns/gandi/internal/types.go @@ -69,7 +69,6 @@ func (r responseFault) faultString() string { return r.FaultString } type responseStruct struct { responseFault - StructMembers []struct { Name string `xml:"name"` ValueInt int `xml:"value>int"` @@ -78,13 +77,11 @@ type responseStruct struct { type responseInt struct { responseFault - Value int `xml:"params>param>value>int"` } type responseBool struct { responseFault - Value bool `xml:"params>param>value>boolean"` } diff --git a/providers/dns/gandiv5/gandiv5.go b/providers/dns/gandiv5/gandiv5.go index 15014e207..8b342592b 100644 --- a/providers/dns/gandiv5/gandiv5.go +++ b/providers/dns/gandiv5/gandiv5.go @@ -10,14 +10,16 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/gandiv5/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) +// Gandi API reference: http://doc.livedns.gandi.net/ + +const minTTL = 300 + // Environment variables names. const ( envNamespace = "GANDIV5_" @@ -31,10 +33,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const minTTL = 300 - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // inProgressInfo contains information about an in-progress challenge. type inProgressInfo struct { fieldName string @@ -114,7 +112,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if err != nil { return nil, fmt.Errorf("gandiv5: %w", err) } - client.BaseURL = baseURL } @@ -122,8 +119,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, client: client, @@ -164,7 +159,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { authZone: authZone, fieldName: subDomain, } - return nil } @@ -175,7 +169,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { // acquire lock and retrieve authZone d.inProgressMu.Lock() defer d.inProgressMu.Unlock() - if _, ok := d.inProgressFQDNs[info.EffectiveFQDN]; !ok { // if there is no cleanup information then just return return nil @@ -190,7 +183,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("gandiv5: %w", err) } - return nil } diff --git a/providers/dns/gandiv5/gandiv5.toml b/providers/dns/gandiv5/gandiv5.toml index 31568e89b..4d952b2c2 100644 --- a/providers/dns/gandiv5/gandiv5.toml +++ b/providers/dns/gandiv5/gandiv5.toml @@ -6,7 +6,7 @@ Since = "v0.5.0" Example = ''' GANDIV5_PERSONAL_ACCESS_TOKEN=abcdefghijklmnopqrstuvwx \ -lego --dns gandiv5 -d '*.example.com' -d example.com run +lego --email you@example.com --dns gandiv5 --domains my.example.org run ''' [Configuration] @@ -14,10 +14,10 @@ lego --dns gandiv5 -d '*.example.com' -d example.com run GANDIV5_PERSONAL_ACCESS_TOKEN = "Personal Access Token" GANDIV5_API_KEY = "API key (Deprecated)" [Configuration.Additional] - GANDIV5_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 20)" - GANDIV5_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 1200)" - GANDIV5_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - GANDIV5_HTTP_TIMEOUT = "API request timeout in seconds (Default: 10)" + GANDIV5_POLLING_INTERVAL = "Time between DNS propagation check" + GANDIV5_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + GANDIV5_TTL = "The TTL of the TXT record used for the DNS challenge" + GANDIV5_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://api.gandi.net/docs/livedns/" diff --git a/providers/dns/gandiv5/gandiv5_test.go b/providers/dns/gandiv5/gandiv5_test.go index d6f077243..57fed032e 100644 --- a/providers/dns/gandiv5/gandiv5_test.go +++ b/providers/dns/gandiv5/gandiv5_test.go @@ -1,11 +1,15 @@ package gandiv5 import ( + "fmt" + "io" + "net/http" "net/http/httptest" + "regexp" "testing" + "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/require" ) @@ -35,7 +39,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -92,44 +95,90 @@ func TestNewDNSProviderConfig(t *testing.T) { // TestDNSProvider runs Present and CleanUp against a fake Gandi RPC // Server, whose responses are predetermined for particular requests. func TestDNSProvider(t *testing.T) { - provider := servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.PersonalAccessToken = "123412341234123412341234" - config.BaseURL = server.URL - config.HTTPClient = server.Client() - - return NewDNSProviderConfig(config) + // serverResponses is the JSON Request->Response map used by the + // fake JSON server. + serverResponses := map[string]map[string]string{ + http.MethodGet: { + ``: `{"rrset_ttl":300,"rrset_values":[],"rrset_name":"_acme-challenge.abc.def","rrset_type":"TXT"}`, }, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("Bearer 123412341234123412341234"), - ). - Route("GET /domains/example.com/records/_acme-challenge.abc.def/TXT", - servermock.RawStringResponse(`{"rrset_ttl":300,"rrset_values":[],"rrset_name":"_acme-challenge.abc.def","rrset_type":"TXT"}`)). - Route("PUT /domains/example.com/records/_acme-challenge.abc.def/TXT", - servermock.RawStringResponse(`{"message": "Zone Record Created"}`), - servermock.CheckRequestJSONBody(`{"rrset_ttl":300,"rrset_values":["ezRpBPY8wH8djMLYjX2uCKPwiKDkFZ1SFMJ6ZXGlHrQ"]}`)). - Route("DELETE /domains/example.com/records/_acme-challenge.abc.def/TXT", nil). - Build(t) + http.MethodPut: { + `{"rrset_ttl":300,"rrset_values":["TOKEN"]}`: `{"message": "Zone Record Created"}`, + }, + http.MethodDelete: { + ``: ``, + }, + } fakeKeyAuth := "XXXX" + regexpToken := regexp.MustCompile(`"rrset_values":\[".+"\]`) + + // start fake RPC server + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc("/domains/example.com/records/_acme-challenge.abc.def/TXT", func(rw http.ResponseWriter, req *http.Request) { + log.Infof("request: %s %s", req.Method, req.URL) + + if req.Header.Get("Authorization") != "Bearer 123412341234123412341234" { + http.Error(rw, `{"message": "missing or malformed Authorization"}`, http.StatusUnauthorized) + return + } + + if req.Method == http.MethodPost && req.Header.Get("Content-Type") != "application/json" { + http.Error(rw, `{"message": "invalid content type"}`, http.StatusBadRequest) + return + } + + body, errS := io.ReadAll(req.Body) + if errS != nil { + http.Error(rw, fmt.Sprintf(`{"message": "read body error: %v"}`, errS), http.StatusInternalServerError) + return + } + + body = regexpToken.ReplaceAllLiteral(body, []byte(`"rrset_values":["TOKEN"]`)) + + responses, ok := serverResponses[req.Method] + if !ok { + http.Error(rw, fmt.Sprintf(`{"message": "Server response for request not found: %#q"}`, string(body)), http.StatusInternalServerError) + return + } + + resp := responses[string(body)] + + _, errS = rw.Write([]byte(resp)) + if errS != nil { + http.Error(rw, fmt.Sprintf(`{"message": "failed to write response: %v"}`, errS), http.StatusInternalServerError) + return + } + }) + mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { + log.Infof("request: %s %s", req.Method, req.URL) + http.Error(rw, fmt.Sprintf(`{"message": "URL doesn't match: %s"}`, req.URL), http.StatusNotFound) + }) + // define function to override findZoneByFqdn with fakeFindZoneByFqdn := func(fqdn string) (string, error) { return "example.com.", nil } + config := NewDefaultConfig() + config.PersonalAccessToken = "123412341234123412341234" + config.BaseURL = server.URL + + provider, err := NewDNSProviderConfig(config) + require.NoError(t, err) + // override findZoneByFqdn function savedFindZoneByFqdn := provider.findZoneByFqdn - defer func() { provider.findZoneByFqdn = savedFindZoneByFqdn }() - provider.findZoneByFqdn = fakeFindZoneByFqdn // run Present - err := provider.Present("abc.def.example.com", "", fakeKeyAuth) + err = provider.Present("abc.def.example.com", "", fakeKeyAuth) require.NoError(t, err) // run CleanUp diff --git a/providers/dns/gandiv5/internal/client.go b/providers/dns/gandiv5/internal/client.go index bfb71c9f6..57de9d615 100644 --- a/providers/dns/gandiv5/internal/client.go +++ b/providers/dns/gandiv5/internal/client.go @@ -15,7 +15,10 @@ import ( ) // defaultBaseURL endpoint is the Gandi API endpoint used by Present and CleanUp. -const defaultBaseURL = "https://api.gandi.net/v5/livedns" +const defaultBaseURL = "https://dns.api.gandi.net/api/v5" + +// APIKeyHeader API key header. +const APIKeyHeader = "X-Api-Key" // Related to Personal Access Token. const authorizationHeader = "Authorization" @@ -75,7 +78,6 @@ func (c *Client) getTXTRecord(ctx context.Context, domain, name string) (*Record } txtRecord := &Record{} - err = c.do(req, txtRecord) if err != nil { return nil, fmt.Errorf("unable to get TXT records for domain %s and name %s: %w", domain, name, err) @@ -93,7 +95,6 @@ func (c *Client) addTXTRecord(ctx context.Context, domain, name string, newRecor } message := apiResponse{} - err = c.do(req, &message) if err != nil { return fmt.Errorf("unable to create TXT record for domain %s and name %s: %w", domain, name, err) @@ -115,7 +116,6 @@ func (c *Client) DeleteTXTRecord(ctx context.Context, domain, name string) error } message := apiResponse{} - err = c.do(req, &message) if err != nil { return fmt.Errorf("unable to delete TXT record for domain %s and name %s: %w", domain, name, err) @@ -130,7 +130,7 @@ func (c *Client) DeleteTXTRecord(ctx context.Context, domain, name string) error func (c *Client) do(req *http.Request, result any) error { if c.apiKey != "" { - req.Header.Set(authorizationHeader, "Apikey "+c.apiKey) + req.Header.Set(APIKeyHeader, c.apiKey) } if c.pat != "" { @@ -208,7 +208,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) response := apiResponse{} - err := json.Unmarshal(raw, &response) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/gandiv5/internal/client_test.go b/providers/dns/gandiv5/internal/client_test.go deleted file mode 100644 index 6a4158dcb..000000000 --- a/providers/dns/gandiv5/internal/client_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package internal - -import ( - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -func mockBuilder(apiKey, pat string) *servermock.Builder[*Client] { - checkHeaders := servermock.CheckHeader().WithJSONHeaders() - - if apiKey != "" { - checkHeaders = checkHeaders.WithAuthorization("Apikey secret-apikey") - } else { - checkHeaders = checkHeaders.WithAuthorization("Bearer secret-pat") - } - - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient(apiKey, pat) - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - checkHeaders, - ) -} - -func TestClient_AddTXTRecord(t *testing.T) { - client := mockBuilder("secret-apikey", ""). - Route("GET /domains/example.com/records/foo/TXT", - servermock.ResponseFromFixture("add_txt_record_get.json")). - Route("PUT /domains/example.com/records/foo/TXT", - servermock.ResponseFromFixture("api_response.json"), - servermock.CheckRequestJSONBody(`{"rrset_ttl":120,"rrset_values":["content","value1"]}`)). - Build(t) - - err := client.AddTXTRecord(t.Context(), "example.com", "foo", "content", 120) - require.NoError(t, err) -} - -func TestClient_DeleteTXTRecord(t *testing.T) { - client := mockBuilder("", "secret-pat"). - Route("DELETE /domains/example.com/records/foo/TXT", - servermock.ResponseFromFixture("api_response.json")). - Build(t) - - err := client.DeleteTXTRecord(t.Context(), "example.com", "foo") - require.NoError(t, err) -} diff --git a/providers/dns/gandiv5/internal/fixtures/add_txt_record_get.json b/providers/dns/gandiv5/internal/fixtures/add_txt_record_get.json deleted file mode 100644 index fead6ab0a..000000000 --- a/providers/dns/gandiv5/internal/fixtures/add_txt_record_get.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "rrset_ttl": 120, - "rrset_values": [ - "value1" - ], - "rrset_name": "foo", - "rrset_type": "TXT" -} diff --git a/providers/dns/gandiv5/internal/fixtures/api_response.json b/providers/dns/gandiv5/internal/fixtures/api_response.json deleted file mode 100644 index 47f4352ff..000000000 --- a/providers/dns/gandiv5/internal/fixtures/api_response.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "message": "test", - "uuid": "123456789" -} diff --git a/providers/dns/gcloud/gcloud.toml b/providers/dns/gcloud/gcloud.toml index 63d22bed3..261e35b91 100644 --- a/providers/dns/gcloud/gcloud.toml +++ b/providers/dns/gcloud/gcloud.toml @@ -5,29 +5,12 @@ Code = "gcloud" Since = "v0.3.0" Example = ''' -# Using a service account file -GCE_PROJECT="gc-project-id" \ -GCE_SERVICE_ACCOUNT_FILE="/path/to/svc/account/file.json" \ -lego --dns gcloud -d '*.example.com' -d example.com run - -# Using default credentials with impersonation -GCE_PROJECT="gc-project-id" \ -GCE_IMPERSONATE_SERVICE_ACCOUNT="target-sa@gc-project-id.iam.gserviceaccount.com" \ -lego --dns gcloud -d '*.example.com' -d example.com run - -# Using service account key with impersonation -GCE_PROJECT="gc-project-id" \ -GCE_SERVICE_ACCOUNT_FILE="/path/to/svc/account/file.json" \ -GCE_IMPERSONATE_SERVICE_ACCOUNT="target-sa@gc-project-id.iam.gserviceaccount.com" \ -lego --dns gcloud -d '*.example.com' -d example.com run -''' - -Additional = ''' -Supports service account impersonation to access Google Cloud DNS resources across different projects or with restricted permissions. - -When using impersonation, the source service account must have: -1. The "Service Account Token Creator" role on the source service account -2. The "https://www.googleapis.com/auth/cloud-platform" scope +GCE_PROJECT="gc-project-id" GCE_SERVICE_ACCOUNT_FILE="/path/to/svc/account/file.json" lego \ + --email="abc@email.com" \ + --domains="example.com" \ + --dns="gcloud" \ + --path="${HOME}/.lego" \ + run ''' [Configuration] @@ -39,10 +22,9 @@ When using impersonation, the source service account must have: [Configuration.Additional] GCE_ALLOW_PRIVATE_ZONE = "Allows requested domain to be in private DNS zone, works only with a private ACME server (by default: false)" GCE_ZONE_ID = "Allows to skip the automatic detection of the zone" - GCE_IMPERSONATE_SERVICE_ACCOUNT = "Service account email to impersonate" - GCE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 5)" - GCE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 180)" - GCE_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + GCE_POLLING_INTERVAL = "Time between DNS propagation check" + GCE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + GCE_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://cloud.google.com/dns/api/v1/" diff --git a/providers/dns/gcloud/googlecloud.go b/providers/dns/gcloud/googlecloud.go index 61e8ee66f..7e4cd8d75 100644 --- a/providers/dns/gcloud/googlecloud.go +++ b/providers/dns/gcloud/googlecloud.go @@ -2,7 +2,6 @@ package gcloud import ( - "context" "encoding/json" "errors" "fmt" @@ -12,72 +11,64 @@ import ( "time" "cloud.google.com/go/compute/metadata" - "github.com/cenkalti/backoff/v5" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/platform/wait" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/miekg/dns" - "golang.org/x/oauth2" + "golang.org/x/net/context" "golang.org/x/oauth2/google" - gdns "google.golang.org/api/dns/v1" + "google.golang.org/api/dns/v1" "google.golang.org/api/googleapi" - "google.golang.org/api/impersonate" "google.golang.org/api/option" ) +const ( + changeStatusDone = "done" +) + // Environment variables names. const ( envNamespace = "GCE_" - EnvServiceAccount = envNamespace + "SERVICE_ACCOUNT" - EnvProject = envNamespace + "PROJECT" - EnvZoneID = envNamespace + "ZONE_ID" - EnvAllowPrivateZone = envNamespace + "ALLOW_PRIVATE_ZONE" - EnvDebug = envNamespace + "DEBUG" - EnvImpersonateServiceAccount = envNamespace + "IMPERSONATE_SERVICE_ACCOUNT" + EnvServiceAccount = envNamespace + "SERVICE_ACCOUNT" + EnvProject = envNamespace + "PROJECT" + EnvZoneID = envNamespace + "ZONE_ID" + EnvAllowPrivateZone = envNamespace + "ALLOW_PRIVATE_ZONE" + EnvDebug = envNamespace + "DEBUG" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) -const changeStatusDone = "done" - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { - Debug bool - Project string - ZoneID string - AllowPrivateZone bool - ImpersonateServiceAccount string - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client + Debug bool + Project string + ZoneID string + AllowPrivateZone bool + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - Debug: env.GetOrDefaultBool(EnvDebug, false), - ZoneID: env.GetOrDefaultString(EnvZoneID, ""), - AllowPrivateZone: env.GetOrDefaultBool(EnvAllowPrivateZone, false), - ImpersonateServiceAccount: env.GetOrDefaultString(EnvImpersonateServiceAccount, ""), - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 180*time.Second), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second), + Debug: env.GetOrDefaultBool(EnvDebug, false), + ZoneID: env.GetOrDefaultString(EnvZoneID, ""), + AllowPrivateZone: env.GetOrDefaultBool(EnvAllowPrivateZone, false), + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 180*time.Second), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config - client *gdns.Service + client *dns.Service } // NewDNSProvider returns a DNSProvider instance configured for Google Cloud DNS. @@ -92,8 +83,7 @@ func NewDNSProvider() (*DNSProvider, error) { } // Use default credentials. - project := env.GetOrDefaultString(EnvProject, autodetectProjectID(context.Background())) - + project := env.GetOrDefaultString(EnvProject, autodetectProjectID()) return NewDNSProviderCredentials(project) } @@ -104,15 +94,14 @@ func NewDNSProviderCredentials(project string) (*DNSProvider, error) { return nil, errors.New("googlecloud: project name missing") } + client, err := google.DefaultClient(context.Background(), dns.NdevClouddnsReadwriteScope) + if err != nil { + return nil, fmt.Errorf("googlecloud: unable to get Google Cloud client: %w", err) + } + config := NewDefaultConfig() config.Project = project - - var err error - - config.HTTPClient, err = newClientFromCredentials(context.Background(), config) - if err != nil { - return nil, fmt.Errorf("googlecloud: %w", err) - } + config.HTTPClient = client return NewDNSProviderConfig(config) } @@ -132,24 +121,22 @@ func NewDNSProviderServiceAccountKey(saKey []byte) (*DNSProvider, error) { var datJSON struct { ProjectID string `json:"project_id"` } - err := json.Unmarshal(saKey, &datJSON) if err != nil || datJSON.ProjectID == "" { return nil, errors.New("googlecloud: project ID not found in Google Cloud Service Account file") } - project = datJSON.ProjectID } + conf, err := google.JWTConfigFromJSON(saKey, dns.NdevClouddnsReadwriteScope) + if err != nil { + return nil, fmt.Errorf("googlecloud: unable to acquire config: %w", err) + } + client := conf.Client(context.Background()) + config := NewDefaultConfig() config.Project = project - - var err error - - config.HTTPClient, err = newClientFromServiceAccountKey(context.Background(), config, saKey) - if err != nil { - return nil, fmt.Errorf("googlecloud: %w", err) - } + config.HTTPClient = client return NewDNSProviderConfig(config) } @@ -174,12 +161,11 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("googlecloud: the configuration of the DNS provider is nil") } - if config.HTTPClient == nil { return nil, errors.New("googlecloud: unable to create Google Cloud DNS service: client is nil") } - svc, err := gdns.NewService(context.Background(), option.WithHTTPClient(clientdebug.Wrap(config.HTTPClient))) + svc, err := dns.NewService(context.Background(), option.WithHTTPClient(config.HTTPClient)) if err != nil { return nil, fmt.Errorf("googlecloud: unable to create Google Cloud DNS service: %w", err) } @@ -189,8 +175,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) zone, err := d.getHostedZone(info.EffectiveFQDN) @@ -206,7 +190,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { for _, rrSet := range existingRrSet { var rrd []string - for _, rr := range rrSet.Rrdatas { data := mustUnquote(rr) rrd = append(rrd, data) @@ -216,18 +199,17 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return nil } } - rrSet.Rrdatas = rrd } // Attempt to delete the existing records before adding the new one. if len(existingRrSet) > 0 { - if err = d.applyChanges(ctx, zone, &gdns.Change{Deletions: existingRrSet}); err != nil { + if err = d.applyChanges(zone, &dns.Change{Deletions: existingRrSet}); err != nil { return fmt.Errorf("googlecloud: %w", err) } } - rec := &gdns.ResourceRecordSet{ + rec := &dns.ResourceRecordSet{ Name: info.EffectiveFQDN, Rrdatas: []string{info.Value}, Ttl: int64(d.config.TTL), @@ -243,18 +225,18 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { } } - change := &gdns.Change{ - Additions: []*gdns.ResourceRecordSet{rec}, + change := &dns.Change{ + Additions: []*dns.ResourceRecordSet{rec}, } - if err = d.applyChanges(ctx, zone, change); err != nil { + if err = d.applyChanges(zone, change); err != nil { return fmt.Errorf("googlecloud: %w", err) } return nil } -func (d *DNSProvider) applyChanges(ctx context.Context, zone string, change *gdns.Change) error { +func (d *DNSProvider) applyChanges(zone string, change *dns.Change) error { if d.config.Debug { data, _ := json.Marshal(change) log.Printf("change (Create): %s", string(data)) @@ -268,7 +250,6 @@ func (d *DNSProvider) applyChanges(ctx context.Context, zone string, change *gdn } data, _ := json.Marshal(change) - return fmt.Errorf("failed to perform changes [zone %s, change %s]: %w", zone, string(data), err) } @@ -279,28 +260,24 @@ func (d *DNSProvider) applyChanges(ctx context.Context, zone string, change *gdn chgID := chg.Id // wait for change to be acknowledged - return wait.Retry(ctx, - func() error { - if d.config.Debug { - data, _ := json.Marshal(change) - log.Printf("change (Get): %s", string(data)) - } + return wait.For("apply change", 30*time.Second, 3*time.Second, func() (bool, error) { + if d.config.Debug { + data, _ := json.Marshal(change) + log.Printf("change (Get): %s", string(data)) + } - chg, err = d.client.Changes.Get(d.config.Project, zone, chgID).Do() - if err != nil { - data, _ := json.Marshal(change) - return fmt.Errorf("failed to get changes [zone %s, change %s]: %w", zone, string(data), err) - } + chg, err = d.client.Changes.Get(d.config.Project, zone, chgID).Do() + if err != nil { + data, _ := json.Marshal(change) + return false, fmt.Errorf("failed to get changes [zone %s, change %s]: %w", zone, string(data), err) + } - if chg.Status != changeStatusDone { - return fmt.Errorf("status: %s", chg.Status) - } + if chg.Status == changeStatusDone { + return true, nil + } - return nil - }, - backoff.WithBackOff(backoff.NewConstantBackOff(3*time.Second)), - backoff.WithMaxElapsedTime(30*time.Second), - ) + return false, fmt.Errorf("status: %s", chg.Status) + }) } // CleanUp removes the TXT record matching the specified parameters. @@ -321,11 +298,10 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return nil } - _, err = d.client.Changes.Create(d.config.Project, zone, &gdns.Change{Deletions: records}).Do() + _, err = d.client.Changes.Create(d.config.Project, zone, &dns.Change{Deletions: records}).Do() if err != nil { return fmt.Errorf("googlecloud: %w", err) } - return nil } @@ -371,7 +347,7 @@ func (d *DNSProvider) getHostedZone(domain string) (string, error) { // (gcloud projects get-iam-policy $project_id) (a role with permission dns.managedZones.list) // // If we force a zone list to succeed, we demand more permissions than needed. -func (d *DNSProvider) lookupHostedZoneID(domain string) (string, []*gdns.ManagedZone, error) { +func (d *DNSProvider) lookupHostedZoneID(domain string) (string, []*dns.ManagedZone, error) { // GCE_ZONE_ID override for service accounts to avoid needing zones-list permission if d.config.ZoneID != "" { zone, err := d.client.ManagedZones.Get(d.config.Project, d.config.ZoneID).Do() @@ -379,10 +355,10 @@ func (d *DNSProvider) lookupHostedZoneID(domain string) (string, []*gdns.Managed return "", nil, fmt.Errorf("API call ManagedZones.Get for explicit zone ID %q in project %q failed: %w", d.config.ZoneID, d.config.Project, err) } - return zone.DnsName, []*gdns.ManagedZone{zone}, nil + return zone.DnsName, []*dns.ManagedZone{zone}, nil } - authZone, err := dns01.FindZoneByFqdn(dns.Fqdn(domain)) + authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) if err != nil { return "", nil, fmt.Errorf("could not find zone: %w", err) } @@ -398,7 +374,7 @@ func (d *DNSProvider) lookupHostedZoneID(domain string) (string, []*gdns.Managed return authZone, zones.ManagedZones, nil } -func (d *DNSProvider) findTxtRecords(zone, fqdn string) ([]*gdns.ResourceRecordSet, error) { +func (d *DNSProvider) findTxtRecords(zone, fqdn string) ([]*dns.ResourceRecordSet, error) { recs, err := d.client.ResourceRecordSets.List(d.config.Project, zone).Name(fqdn).Type("TXT").Do() if err != nil { return nil, err @@ -407,65 +383,16 @@ func (d *DNSProvider) findTxtRecords(zone, fqdn string) ([]*gdns.ResourceRecordS return recs.Rrsets, nil } -func newClientFromCredentials(ctx context.Context, config *Config) (*http.Client, error) { - if config.ImpersonateServiceAccount != "" { - ts, err := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/cloud-platform") - if err != nil { - return nil, fmt.Errorf("unable to get default token source: %w", err) - } - - return newImpersonateClient(ctx, config.ImpersonateServiceAccount, ts) - } - - client, err := google.DefaultClient(ctx, gdns.NdevClouddnsReadwriteScope) - if err != nil { - return nil, fmt.Errorf("unable to get Google Cloud client: %w", err) - } - - return client, nil -} - -func newClientFromServiceAccountKey(ctx context.Context, config *Config, saKey []byte) (*http.Client, error) { - if config.ImpersonateServiceAccount != "" { - conf, err := google.JWTConfigFromJSON(saKey, "https://www.googleapis.com/auth/cloud-platform") - if err != nil { - return nil, fmt.Errorf("unable to acquire config: %w", err) - } - - return newImpersonateClient(ctx, config.ImpersonateServiceAccount, conf.TokenSource(ctx)) - } - - conf, err := google.JWTConfigFromJSON(saKey, gdns.NdevClouddnsReadwriteScope) - if err != nil { - return nil, fmt.Errorf("unable to acquire config: %w", err) - } - - return conf.Client(ctx), nil -} - -func newImpersonateClient(ctx context.Context, impersonateServiceAccount string, ts oauth2.TokenSource) (*http.Client, error) { - impersonatedTS, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{ - TargetPrincipal: impersonateServiceAccount, - Scopes: []string{gdns.NdevClouddnsReadwriteScope}, - }, option.WithTokenSource(ts)) - if err != nil { - return nil, fmt.Errorf("unable to create impersonated credentials: %w", err) - } - - return oauth2.NewClient(ctx, impersonatedTS), nil -} - func mustUnquote(raw string) string { clean, err := strconv.Unquote(raw) if err != nil { return raw } - return clean } -func autodetectProjectID(ctx context.Context) string { - if pid, err := metadata.ProjectIDWithContext(ctx); err == nil { +func autodetectProjectID() string { + if pid, err := metadata.ProjectID(); err == nil { return pid } diff --git a/providers/dns/gcloud/googlecloud_test.go b/providers/dns/gcloud/googlecloud_test.go index 28b08a2f9..453fdd5ed 100644 --- a/providers/dns/gcloud/googlecloud_test.go +++ b/providers/dns/gcloud/googlecloud_test.go @@ -1,7 +1,6 @@ package gcloud import ( - "context" "encoding/json" "fmt" "net/http" @@ -11,8 +10,8 @@ import ( "time" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/require" + "golang.org/x/net/context" "golang.org/x/oauth2/google" "google.golang.org/api/dns/v1" ) @@ -31,8 +30,7 @@ var envTest = tester.NewEnvTest( envServiceAccountFile, envGoogleApplicationCredentials, envMetadataHost, - EnvServiceAccount, - EnvImpersonateServiceAccount). + EnvServiceAccount). WithDomain(envDomain). WithLiveTestExtra(func() bool { _, err := google.DefaultClient(context.Background(), dns.NdevClouddnsReadwriteScope) @@ -52,7 +50,7 @@ func TestNewDNSProvider(t *testing.T) { envServiceAccountFile: "", // as Travis run on GCE, we have to alter env envGoogleApplicationCredentials: "not-a-secret-file", - envMetadataHost: "http://example.com", // defined here to avoid the client cache. + envMetadataHost: "http://lego.wtf", // defined here to avoid the client cache. }, // the error message varies according to the OS used. expected: "googlecloud: unable to get Google Cloud client: google: error getting credentials using GOOGLE_APPLICATION_CREDENTIALS environment variable: ", @@ -63,7 +61,7 @@ func TestNewDNSProvider(t *testing.T) { EnvProject: "", envServiceAccountFile: "", // as Travis run on GCE, we have to alter env - envMetadataHost: "http://example.com", + envMetadataHost: "http://lego.wtf", }, expected: "googlecloud: project name missing", }, @@ -86,7 +84,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -126,7 +123,6 @@ func TestNewDNSProviderConfig(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() config := NewDefaultConfig() @@ -147,162 +143,245 @@ func TestNewDNSProviderConfig(t *testing.T) { } func TestPresentNoExistingRR(t *testing.T) { - provider := mockBuilder(). - // getHostedZone - Route("GET /dns/v1/projects/manhattan/managedZones", - servermock.JSONEncode(&dns.ManagedZonesListResponse{ - ManagedZones: []*dns.ManagedZone{ - {Name: "test", Visibility: "public"}, - }, - }), - servermock.CheckQueryParameter().Strict(). - With("dnsName", "example.com."). - With("prettyPrint", "false"). - With("alt", "json")). - // findTxtRecords - Route("GET /dns/v1/projects/manhattan/managedZones/test/rrsets", - servermock.JSONEncode(&dns.ResourceRecordSetsListResponse{ - Rrsets: []*dns.ResourceRecordSet{}, - }), - servermock.CheckQueryParameter().Strict(). - With("name", "_acme-challenge.example.com."). - With("type", "TXT"). - With("prettyPrint", "false"). - With("alt", "json")). - // applyChanges [Create] - Route("POST /dns/v1/projects/manhattan/managedZones/test/changes", - http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - var chgReq dns.Change - if err := json.NewDecoder(req.Body).Decode(&chgReq); err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) - return - } + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) - chgResp := chgReq - chgResp.Status = changeStatusDone + // getHostedZone: /manhattan/managedZones?alt=json&dnsName=lego.wtf. + mux.HandleFunc("/dns/v1/projects/manhattan/managedZones", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } - if err := json.NewEncoder(rw).Encode(chgResp); err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - }), - servermock.CheckQueryParameter().Strict(). - With("prettyPrint", "false"). - With("alt", "json")). - Build(t) + mzlrs := &dns.ManagedZonesListResponse{ + ManagedZones: []*dns.ManagedZone{ + {Name: "test", Visibility: "public"}, + }, + } - domain := "example.com" + err := json.NewEncoder(w).Encode(mzlrs) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) - err := provider.Present(domain, "", "") + // findTxtRecords: /manhattan/managedZones/test/rrsets?alt=json&name=_acme-challenge.lego.wtf.&type=TXT + mux.HandleFunc("/dns/v1/projects/manhattan/managedZones/test/rrsets", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + rrslr := &dns.ResourceRecordSetsListResponse{ + Rrsets: []*dns.ResourceRecordSet{}, + } + + err := json.NewEncoder(w).Encode(rrslr) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + // applyChanges [Create]: /manhattan/managedZones/test/changes?alt=json + mux.HandleFunc("/dns/v1/projects/manhattan/managedZones/test/changes", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + var chgReq dns.Change + if err := json.NewDecoder(r.Body).Decode(&chgReq); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + chgResp := chgReq + chgResp.Status = changeStatusDone + + if err := json.NewEncoder(w).Encode(chgResp); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + config := NewDefaultConfig() + config.HTTPClient = &http.Client{Timeout: 10 * time.Second} + config.Project = "manhattan" + + p, err := NewDNSProviderConfig(config) + require.NoError(t, err) + + p.client.BasePath = server.URL + + domain := "lego.wtf" + + err = p.Present(domain, "", "") require.NoError(t, err) } func TestPresentWithExistingRR(t *testing.T) { - provider := mockBuilder(). - // getHostedZone - Route("GET /dns/v1/projects/manhattan/managedZones", - servermock.JSONEncode(&dns.ManagedZonesListResponse{ - ManagedZones: []*dns.ManagedZone{ - {Name: "test", Visibility: "public"}, - }, - }), - servermock.CheckQueryParameter().Strict(). - With("dnsName", "example.com."). - With("prettyPrint", "false"). - With("alt", "json")). - // findTxtRecords - Route("GET /dns/v1/projects/manhattan/managedZones/test/rrsets", - servermock.JSONEncode(&dns.ResourceRecordSetsListResponse{ - Rrsets: []*dns.ResourceRecordSet{{ - Name: "_acme-challenge.example.com.", - Rrdatas: []string{`"X7DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"`, `"huji"`}, - Ttl: 120, - Type: "TXT", - }}, - }), - servermock.CheckQueryParameter().Strict(). - With("name", "_acme-challenge.example.com."). - With("type", "TXT"). - With("prettyPrint", "false"). - With("alt", "json")). - // applyChanges [Create] - Route("POST /dns/v1/projects/manhattan/managedZones/test/changes", - http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - var chgReq dns.Change - if err := json.NewDecoder(req.Body).Decode(&chgReq); err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + // getHostedZone: /manhattan/managedZones?alt=json&dnsName=lego.wtf. + mux.HandleFunc("/dns/v1/projects/manhattan/managedZones", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + mzlrs := &dns.ManagedZonesListResponse{ + ManagedZones: []*dns.ManagedZone{ + {Name: "test", Visibility: "public"}, + }, + } + + err := json.NewEncoder(w).Encode(mzlrs) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + // findTxtRecords: /manhattan/managedZones/test/rrsets?alt=json&name=_acme-challenge.lego.wtf.&type=TXT + mux.HandleFunc("/dns/v1/projects/manhattan/managedZones/test/rrsets", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + rrslr := &dns.ResourceRecordSetsListResponse{ + Rrsets: []*dns.ResourceRecordSet{{ + Name: "_acme-challenge.lego.wtf.", + Rrdatas: []string{`"X7DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"`, `"huji"`}, + Ttl: 120, + Type: "TXT", + }}, + } + + err := json.NewEncoder(w).Encode(rrslr) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + // applyChanges [Create]: /manhattan/managedZones/test/changes?alt=json + mux.HandleFunc("/dns/v1/projects/manhattan/managedZones/test/changes", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + var chgReq dns.Change + if err := json.NewDecoder(r.Body).Decode(&chgReq); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if len(chgReq.Additions) > 0 { + sort.Strings(chgReq.Additions[0].Rrdatas) + } + + var prevVal string + for _, addition := range chgReq.Additions { + for _, value := range addition.Rrdatas { + if prevVal == value { + http.Error(w, fmt.Sprintf("The resource %s already exists", value), http.StatusConflict) return } + prevVal = value + } + } - if len(chgReq.Additions) > 0 { - sort.Strings(chgReq.Additions[0].Rrdatas) - } + chgResp := chgReq + chgResp.Status = changeStatusDone - var prevVal string + if err := json.NewEncoder(w).Encode(chgResp); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) - for _, addition := range chgReq.Additions { - for _, value := range addition.Rrdatas { - if prevVal == value { - http.Error(rw, fmt.Sprintf("The resource %s already exists", value), http.StatusConflict) - return - } + config := NewDefaultConfig() + config.HTTPClient = &http.Client{Timeout: 10 * time.Second} + config.Project = "manhattan" - prevVal = value - } - } + p, err := NewDNSProviderConfig(config) + require.NoError(t, err) - chgResp := chgReq - chgResp.Status = changeStatusDone + p.client.BasePath = server.URL - if err := json.NewEncoder(rw).Encode(chgResp); err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - }), - servermock.CheckQueryParameter().Strict(). - With("prettyPrint", "false"). - With("alt", "json")). - Build(t) + domain := "lego.wtf" - domain := "example.com" - - err := provider.Present(domain, "", "") + err = p.Present(domain, "", "") require.NoError(t, err) } func TestPresentSkipExistingRR(t *testing.T) { - provider := mockBuilder(). - // getHostedZone - Route("GET /dns/v1/projects/manhattan/managedZones", - servermock.JSONEncode(&dns.ManagedZonesListResponse{ - ManagedZones: []*dns.ManagedZone{ - {Name: "test", Visibility: "public"}, - }, - }), - servermock.CheckQueryParameter().Strict(). - With("dnsName", "example.com."). - With("prettyPrint", "false"). - With("alt", "json")). - // findTxtRecords - Route("GET /dns/v1/projects/manhattan/managedZones/test/rrsets", - servermock.JSONEncode(&dns.ResourceRecordSetsListResponse{ - Rrsets: []*dns.ResourceRecordSet{{ - Name: "_acme-challenge.example.com.", - Rrdatas: []string{`"47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"`, `"X7DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"`, `"huji"`}, - Ttl: 120, - Type: "TXT", - }}, - }), - servermock.CheckQueryParameter().Strict(). - With("name", "_acme-challenge.example.com."). - With("type", "TXT"). - With("prettyPrint", "false"). - With("alt", "json")). - Build(t) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) - domain := "example.com" + // getHostedZone: /manhattan/managedZones?alt=json&dnsName=lego.wtf. + mux.HandleFunc("/dns/v1/projects/manhattan/managedZones", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } - err := provider.Present(domain, "", "") + mzlrs := &dns.ManagedZonesListResponse{ + ManagedZones: []*dns.ManagedZone{ + {Name: "test", Visibility: "public"}, + }, + } + + err := json.NewEncoder(w).Encode(mzlrs) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + // findTxtRecords: /manhattan/managedZones/test/rrsets?alt=json&name=_acme-challenge.lego.wtf.&type=TXT + mux.HandleFunc("/dns/v1/projects/manhattan/managedZones/test/rrsets", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + rrslr := &dns.ResourceRecordSetsListResponse{ + Rrsets: []*dns.ResourceRecordSet{{ + Name: "_acme-challenge.lego.wtf.", + Rrdatas: []string{`"47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"`, `"X7DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"`, `"huji"`}, + Ttl: 120, + Type: "TXT", + }}, + } + + err := json.NewEncoder(w).Encode(rrslr) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + config := NewDefaultConfig() + config.HTTPClient = &http.Client{Timeout: 10 * time.Second} + config.Project = "manhattan" + + p, err := NewDNSProviderConfig(config) + require.NoError(t, err) + + p.client.BasePath = server.URL + + domain := "lego.wtf" + + err = p.Present(domain, "", "") require.NoError(t, err) } @@ -352,20 +431,3 @@ func TestLiveCleanUp(t *testing.T) { err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.HTTPClient = server.Client() - config.Project = "manhattan" - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - p.client.BasePath = server.URL - - return p, err - }) -} diff --git a/providers/dns/gcore/gcore.go b/providers/dns/gcore/gcore.go index 9b98f28d4..fe427647f 100644 --- a/providers/dns/gcore/gcore.go +++ b/providers/dns/gcore/gcore.go @@ -1,16 +1,21 @@ -// Package gcore implements a DNS provider for solving the DNS-01 challenge using G-Core. package gcore import ( + "context" "errors" "fmt" "net/http" + "strings" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/gcore" + "github.com/go-acme/lego/v4/providers/dns/gcore/internal" +) + +const ( + defaultPropagationTimeout = 360 * time.Second + defaultPollingInterval = 20 * time.Second ) // Environment variables names. @@ -25,17 +30,21 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config for DNSProvider. -type Config = gcore.Config +type Config struct { + APIToken string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, gcore.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, gcore.DefaultPollingInterval), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, defaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, defaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), }, @@ -44,7 +53,8 @@ func NewDefaultConfig() *Config { // DNSProvider an implementation of challenge.Provider contract. type DNSProvider struct { - prv challenge.ProviderTimeout + config *Config + client *internal.Client } // NewDNSProvider returns an instance of DNSProvider configured for G-Core DNS API. @@ -66,36 +76,91 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("gcore: the configuration of the DNS provider is nil") } - provider, err := gcore.NewDNSProviderConfig(config, "") - if err != nil { - return nil, fmt.Errorf("gcore: %w", err) + if config.APIToken == "" { + return nil, errors.New("gcore: incomplete credentials provided") } - return &DNSProvider{prv: provider}, nil + client := internal.NewClient(config.APIToken) + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + return &DNSProvider{ + config: config, + client: client, + }, nil } -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - err := d.prv.Present(domain, token, keyAuth) +// Present creates a TXT record to fulfill the dns-01 challenge. +func (d *DNSProvider) Present(domain, _, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + zone, err := d.guessZone(ctx, info.EffectiveFQDN) if err != nil { return fmt.Errorf("gcore: %w", err) } + err = d.client.AddRRSet(ctx, zone, dns01.UnFqdn(info.EffectiveFQDN), info.Value, d.config.TTL) + if err != nil { + return fmt.Errorf("gcore: add txt record: %w", err) + } + return nil } -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - err := d.prv.CleanUp(domain, token, keyAuth) +// CleanUp removes the record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + zone, err := d.guessZone(ctx, info.EffectiveFQDN) if err != nil { return fmt.Errorf("gcore: %w", err) } + err = d.client.DeleteRRSet(ctx, zone, dns01.UnFqdn(info.EffectiveFQDN)) + if err != nil { + return fmt.Errorf("gcore: remove txt record: %w", err) + } + return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.prv.Timeout() + return d.config.PropagationTimeout, d.config.PollingInterval +} + +func (d *DNSProvider) guessZone(ctx context.Context, fqdn string) (string, error) { + var lastErr error + + for _, zone := range extractAllZones(fqdn) { + dnsZone, err := d.client.GetZone(ctx, zone) + if err == nil { + return dnsZone.Name, nil + } + + lastErr = err + } + + return "", fmt.Errorf("zone %q not found: %w", fqdn, lastErr) +} + +func extractAllZones(fqdn string) []string { + parts := strings.Split(dns01.UnFqdn(fqdn), ".") + if len(parts) < 3 { + return nil + } + + var zones []string + for i := 1; i < len(parts)-1; i++ { + zones = append(zones, strings.Join(parts[i:], ".")) + } + + return zones } diff --git a/providers/dns/gcore/gcore.toml b/providers/dns/gcore/gcore.toml index 983c35f8a..121a6d882 100644 --- a/providers/dns/gcore/gcore.toml +++ b/providers/dns/gcore/gcore.toml @@ -6,17 +6,17 @@ Since = "v4.5.0" Example = ''' GCORE_PERMANENT_API_TOKEN=xxxxx \ -lego --dns gcore -d '*.example.com' -d example.com run +lego --email you@example.com --dns gcore --domains my.example.org run ''' [Configuration] [Configuration.Credentials] GCORE_PERMANENT_API_TOKEN = "Permanent API token (https://gcore.com/blog/permanent-api-token-explained/)" [Configuration.Additional] - GCORE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 20)" - GCORE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 360)" - GCORE_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - GCORE_HTTP_TIMEOUT = "API request timeout in seconds (Default: 10)" + GCORE_POLLING_INTERVAL = "Time between DNS propagation check" + GCORE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + GCORE_TTL = "The TTL of the TXT record used for the DNS challenge" + GCORE_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://api.gcore.com/docs/dns#tag/zones" diff --git a/providers/dns/gcore/gcore_test.go b/providers/dns/gcore/gcore_test.go index 6f8e38c12..a5eddee7c 100644 --- a/providers/dns/gcore/gcore_test.go +++ b/providers/dns/gcore/gcore_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -33,7 +34,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -43,7 +43,8 @@ func TestNewDNSProvider(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - require.NotNil(t, p.prv) + require.NotNil(t, p.config) + require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } @@ -77,7 +78,8 @@ func TestNewDNSProviderConfig(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - require.NotNil(t, p.prv) + require.NotNil(t, p.config) + require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } @@ -91,7 +93,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -105,10 +106,36 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } + +func Test_extractAllZones(t *testing.T) { + testCases := []struct { + desc string + fqdn string + expected []string + }{ + { + desc: "success", + fqdn: "_acme-challenge.my.test.domain.com.", + expected: []string{"my.test.domain.com", "test.domain.com", "domain.com"}, + }, + { + desc: "empty", + fqdn: "_acme-challenge.com.", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := extractAllZones(test.fqdn) + assert.Equal(t, test.expected, got) + }) + } +} diff --git a/providers/dns/internal/gcore/internal/client.go b/providers/dns/gcore/internal/client.go similarity index 91% rename from providers/dns/internal/gcore/internal/client.go rename to providers/dns/gcore/internal/client.go index f3ad4e461..085b4d6cb 100644 --- a/providers/dns/internal/gcore/internal/client.go +++ b/providers/dns/gcore/internal/client.go @@ -27,7 +27,7 @@ const txtRecordType = "TXT" type Client struct { token string - BaseURL *url.URL + baseURL *url.URL HTTPClient *http.Client } @@ -37,7 +37,7 @@ func NewClient(token string) *Client { return &Client{ token: token, - BaseURL: baseURL, + baseURL: baseURL, HTTPClient: &http.Client{Timeout: 10 * time.Second}, } } @@ -45,10 +45,9 @@ func NewClient(token string) *Client { // GetZone gets zone information. // https://api.gcore.com/docs/dns#tag/zones/operation/Zone func (c *Client) GetZone(ctx context.Context, name string) (Zone, error) { - endpoint := c.BaseURL.JoinPath("v2", "zones", name) + endpoint := c.baseURL.JoinPath("v2", "zones", name) zone := Zone{} - err := c.doRequest(ctx, http.MethodGet, endpoint, nil, &zone) if err != nil { return Zone{}, fmt.Errorf("get zone %s: %w", name, err) @@ -60,10 +59,9 @@ func (c *Client) GetZone(ctx context.Context, name string) (Zone, error) { // GetRRSet gets RRSet item. // https://api.gcore.com/docs/dns#tag/rrsets/operation/RRSet func (c *Client) GetRRSet(ctx context.Context, zone, name string) (RRSet, error) { - endpoint := c.BaseURL.JoinPath("v2", "zones", zone, name, txtRecordType) + endpoint := c.baseURL.JoinPath("v2", "zones", zone, name, txtRecordType) var result RRSet - err := c.doRequest(ctx, http.MethodGet, endpoint, nil, &result) if err != nil { return RRSet{}, fmt.Errorf("get txt records %s -> %s: %w", zone, name, err) @@ -75,7 +73,7 @@ func (c *Client) GetRRSet(ctx context.Context, zone, name string) (RRSet, error) // DeleteRRSet removes RRSet record. // https://api.gcore.com/docs/dns#tag/rrsets/operation/DeleteRRSet func (c *Client) DeleteRRSet(ctx context.Context, zone, name string) error { - endpoint := c.BaseURL.JoinPath("v2", "zones", zone, name, txtRecordType) + endpoint := c.baseURL.JoinPath("v2", "zones", zone, name, txtRecordType) err := c.doRequest(ctx, http.MethodDelete, endpoint, nil, nil) if err != nil { @@ -106,19 +104,19 @@ func (c *Client) AddRRSet(ctx context.Context, zone, recordName, value string, t // https://api.gcore.com/docs/dns#tag/rrsets/operation/CreateRRSet func (c *Client) createRRSet(ctx context.Context, zone, name string, record RRSet) error { - endpoint := c.BaseURL.JoinPath("v2", "zones", zone, name, txtRecordType) + endpoint := c.baseURL.JoinPath("v2", "zones", zone, name, txtRecordType) return c.doRequest(ctx, http.MethodPost, endpoint, record, nil) } // https://api.gcore.com/docs/dns#tag/rrsets/operation/UpdateRRSet func (c *Client) updateRRSet(ctx context.Context, zone, name string, record RRSet) error { - endpoint := c.BaseURL.JoinPath("v2", "zones", zone, name, txtRecordType) + endpoint := c.baseURL.JoinPath("v2", "zones", zone, name, txtRecordType) return c.doRequest(ctx, http.MethodPut, endpoint, record, nil) } -func (c *Client) doRequest(ctx context.Context, method string, endpoint *url.URL, bodyParams, result any) error { +func (c *Client) doRequest(ctx context.Context, method string, endpoint *url.URL, bodyParams any, result any) error { req, err := newJSONRequest(ctx, method, endpoint, bodyParams) if err != nil { return fmt.Errorf("new request: %w", err) @@ -182,7 +180,6 @@ func parseError(resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) errAPI := APIError{StatusCode: resp.StatusCode} - err := json.Unmarshal(raw, &errAPI) if err != nil { errAPI.Message = string(raw) diff --git a/providers/dns/gcore/internal/client_test.go b/providers/dns/gcore/internal/client_test.go new file mode 100644 index 000000000..f414b33e1 --- /dev/null +++ b/providers/dns/gcore/internal/client_test.go @@ -0,0 +1,256 @@ +package internal + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + testToken = "test" + testRecordContent = "acme" + testRecordContent2 = "foo" + testTTL = 10 +) + +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient(testToken) + client.baseURL, _ = url.Parse(server.URL) + + return client, mux +} + +func TestClient_GetZone(t *testing.T) { + client, mux := setupTest(t) + + expected := Zone{Name: "example.com"} + + mux.Handle("/v2/zones/example.com", validationHandler{ + method: http.MethodGet, + next: handleJSONResponse(expected), + }) + + zone, err := client.GetZone(context.Background(), "example.com") + require.NoError(t, err) + + assert.Equal(t, expected, zone) +} + +func TestClient_GetZone_error(t *testing.T) { + client, mux := setupTest(t) + + mux.Handle("/v2/zones/example.com", validationHandler{ + method: http.MethodGet, + next: handleAPIError(), + }) + + _, err := client.GetZone(context.Background(), "example.com") + require.Error(t, err) +} + +func TestClient_GetRRSet(t *testing.T) { + client, mux := setupTest(t) + + expected := RRSet{ + TTL: testTTL, + Records: []Records{ + {Content: []string{testRecordContent}}, + }, + } + + mux.Handle("/v2/zones/example.com/foo.example.com/TXT", validationHandler{ + method: http.MethodGet, + next: handleJSONResponse(expected), + }) + + rrSet, err := client.GetRRSet(context.Background(), "example.com", "foo.example.com") + require.NoError(t, err) + + assert.Equal(t, expected, rrSet) +} + +func TestClient_GetRRSet_error(t *testing.T) { + client, mux := setupTest(t) + + mux.Handle("/v2/zones/example.com/foo.example.com/TXT", validationHandler{ + method: http.MethodGet, + next: handleAPIError(), + }) + + _, err := client.GetRRSet(context.Background(), "example.com", "foo.example.com") + require.Error(t, err) +} + +func TestClient_DeleteRRSet(t *testing.T) { + client, mux := setupTest(t) + + mux.Handle("/v2/zones/test.example.com/my.test.example.com/"+txtRecordType, + validationHandler{method: http.MethodDelete}) + + err := client.DeleteRRSet(context.Background(), "test.example.com", "my.test.example.com.") + require.NoError(t, err) +} + +func TestClient_DeleteRRSet_error(t *testing.T) { + client, mux := setupTest(t) + + mux.Handle("/v2/zones/test.example.com/my.test.example.com/"+txtRecordType, validationHandler{ + method: http.MethodDelete, + next: handleAPIError(), + }) + + err := client.DeleteRRSet(context.Background(), "test.example.com", "my.test.example.com.") + require.NoError(t, err) +} + +func TestClient_AddRRSet(t *testing.T) { + testCases := []struct { + desc string + zone string + recordName string + value string + handledDomain string + handlers map[string]http.Handler + wantErr bool + }{ + { + desc: "success add", + zone: "test.example.com", + recordName: "my.test.example.com", + value: testRecordContent, + handlers: map[string]http.Handler{ + // createRRSet + "/v2/zones/test.example.com/my.test.example.com/" + txtRecordType: validationHandler{ + method: http.MethodPost, + next: handleAddRRSet([]Records{{Content: []string{testRecordContent}}}), + }, + }, + }, + { + desc: "success update", + zone: "test.example.com", + recordName: "my.test.example.com", + value: testRecordContent, + handlers: map[string]http.Handler{ + "/v2/zones/test.example.com/my.test.example.com/" + txtRecordType: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + switch req.Method { + case http.MethodGet: // GetRRSet + data := RRSet{ + TTL: testTTL, + Records: []Records{{Content: []string{testRecordContent2}}}, + } + handleJSONResponse(data).ServeHTTP(rw, req) + case http.MethodPut: // updateRRSet + expected := []Records{ + {Content: []string{testRecordContent}}, + {Content: []string{testRecordContent2}}, + } + handleAddRRSet(expected).ServeHTTP(rw, req) + default: + http.Error(rw, "wrong method", http.StatusMethodNotAllowed) + } + }), + }, + }, + { + desc: "not in the zone", + zone: "test.example.com", + recordName: "notfound.example.com", + value: testRecordContent, + wantErr: true, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + cl, mux := setupTest(t) + + for pattern, handler := range test.handlers { + mux.Handle(pattern, handler) + } + + err := cl.AddRRSet(context.Background(), test.zone, test.recordName, test.value, testTTL) + if test.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + }) + } +} + +type validationHandler struct { + method string + next http.Handler +} + +func (v validationHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if req.Header.Get(authorizationHeader) != fmt.Sprintf("%s %s", tokenTypeHeader, testToken) { + rw.WriteHeader(http.StatusForbidden) + _ = json.NewEncoder(rw).Encode(APIError{Message: "token up for parsing was not passed through the context"}) + return + } + + if req.Method != v.method { + http.Error(rw, "wrong method", http.StatusMethodNotAllowed) + return + } + + if v.next != nil { + v.next.ServeHTTP(rw, req) + } +} + +func handleAPIError() http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusInternalServerError) + _ = json.NewEncoder(rw).Encode(APIError{Message: "oops"}) + } +} + +func handleJSONResponse(data interface{}) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + err := json.NewEncoder(rw).Encode(data) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + } +} + +func handleAddRRSet(expected []Records) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + body := RRSet{} + + err := json.NewDecoder(req.Body).Decode(&body) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + if body.TTL != testTTL { + http.Error(rw, "wrong ttl", http.StatusInternalServerError) + return + } + + if !reflect.DeepEqual(body.Records, expected) { + http.Error(rw, "wrong resource records", http.StatusInternalServerError) + return + } + } +} diff --git a/providers/dns/internal/gcore/internal/types.go b/providers/dns/gcore/internal/types.go similarity index 100% rename from providers/dns/internal/gcore/internal/types.go rename to providers/dns/gcore/internal/types.go diff --git a/providers/dns/gigahostno/gigahostno.go b/providers/dns/gigahostno/gigahostno.go deleted file mode 100644 index b9ed23f3f..000000000 --- a/providers/dns/gigahostno/gigahostno.go +++ /dev/null @@ -1,233 +0,0 @@ -// Package gigahostno implements a DNS provider for solving the DNS-01 challenge using Gigahost.no. -package gigahostno - -import ( - "context" - "errors" - "fmt" - "net/http" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/gigahostno/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "GIGAHOSTNO_" - - EnvUsername = envNamespace + "USERNAME" - EnvPassword = envNamespace + "PASSWORD" - EnvSecret = envNamespace + "SECRET" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - Username string - Password string - Secret string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - - identifier *internal.Identifier - client *internal.Client - - tokenMu sync.Mutex - token *internal.Token -} - -// NewDNSProvider returns a DNSProvider instance configured for Gigahost. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvUsername, EnvPassword) - if err != nil { - return nil, fmt.Errorf("gigahostno: %w", err) - } - - config := NewDefaultConfig() - config.Username = values[EnvUsername] - config.Password = values[EnvPassword] - config.Secret = env.GetOrFile(EnvSecret) - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Gigahost. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("gigahostno: the configuration of the DNS provider is nil") - } - - identifier, err := internal.NewIdentifier(config.Username, config.Password, config.Secret) - if err != nil { - return nil, fmt.Errorf("gigahostno: %w", err) - } - - if config.HTTPClient != nil { - identifier.HTTPClient = config.HTTPClient - } - - identifier.HTTPClient = clientdebug.Wrap(identifier.HTTPClient) - - client := internal.NewClient() - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - identifier: identifier, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - err := d.authenticate(ctx) - if err != nil { - return fmt.Errorf("gigahostno: %w", err) - } - - ctx = internal.WithContext(ctx, d.token.Token) - - zone, err := d.findZone(ctx, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("gigahostno: %w", err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name) - if err != nil { - return fmt.Errorf("gigahostno: %w", err) - } - - record := internal.Record{ - Name: subDomain, - Type: "TXT", - Value: info.Value, - TTL: d.config.TTL, - } - - err = d.client.CreateNewRecord(ctx, zone.ID, record) - if err != nil { - return fmt.Errorf("gigahostno: create new record: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - err := d.authenticate(ctx) - if err != nil { - return fmt.Errorf("gigahostno: %w", err) - } - - ctx = internal.WithContext(ctx, d.token.Token) - - zone, err := d.findZone(ctx, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("gigahostno: %w", err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name) - if err != nil { - return fmt.Errorf("gigahostno: %w", err) - } - - records, err := d.client.GetZoneRecords(ctx, zone.ID) - if err != nil { - return fmt.Errorf("gigahostno: get zone records: %w", err) - } - - for _, record := range records { - if record.Type == "TXT" && record.Name == subDomain && record.Value == info.Value { - err := d.client.DeleteRecord(ctx, zone.ID, record.ID, record.Name, record.Type) - if err != nil { - return fmt.Errorf("gigahostno: delete record: %w", err) - } - - break - } - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -func (d *DNSProvider) authenticate(ctx context.Context) error { - d.tokenMu.Lock() - defer d.tokenMu.Unlock() - - if !d.token.IsExpired() { - return nil - } - - tok, err := d.identifier.Authenticate(ctx) - if err != nil { - return fmt.Errorf("authenticate: %w", err) - } - - d.token = tok - - return nil -} - -func (d *DNSProvider) findZone(ctx context.Context, fqdn string) (*internal.Zone, error) { - zones, err := d.client.GetZones(ctx) - if err != nil { - return nil, fmt.Errorf("get zones: %w", err) - } - - for d := range dns01.UnFqdnDomainsSeq(fqdn) { - for _, zone := range zones { - if zone.Name == d && zone.Active == "1" { - return &zone, nil - } - } - } - - return nil, fmt.Errorf("zone not found for %q", fqdn) -} diff --git a/providers/dns/gigahostno/gigahostno.toml b/providers/dns/gigahostno/gigahostno.toml deleted file mode 100644 index b8d3fad2b..000000000 --- a/providers/dns/gigahostno/gigahostno.toml +++ /dev/null @@ -1,25 +0,0 @@ -Name = "Gigahost.no" -Description = '''''' -URL = "https://gigahost.no/" -Code = "gigahostno" -Since = "v4.29.0" - -Example = ''' -GIGAHOSTNO_USERNAME="xxxxxxxxxxxxxxxxxxxxx" \ -GIGAHOSTNO_PASSWORD="yyyyyyyyyyyyyyyyyyyyy" \ -lego --dns gigahostno -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - GIGAHOSTNO_USERNAME = "Username" - GIGAHOSTNO_PASSWORD = "Password" - [Configuration.Additional] - GIGAHOSTNO_SECRET = "TOTP secret" - GIGAHOSTNO_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - GIGAHOSTNO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - GIGAHOSTNO_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - GIGAHOSTNO_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://gigahost.no/api-dokumentasjon" diff --git a/providers/dns/gigahostno/gigahostno_test.go b/providers/dns/gigahostno/gigahostno_test.go deleted file mode 100644 index 7aaac0159..000000000 --- a/providers/dns/gigahostno/gigahostno_test.go +++ /dev/null @@ -1,277 +0,0 @@ -package gigahostno - -import ( - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/go-acme/lego/v4/providers/dns/gigahostno/internal" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest( - EnvUsername, - EnvPassword, - EnvSecret, -).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvUsername: "user", - EnvPassword: "secret", - EnvSecret: "super-secret", - }, - }, - { - desc: "missing GIGAHOSTNO_USERNAME", - envVars: map[string]string{ - EnvPassword: "secret", - }, - expected: "gigahostno: some credentials information are missing: GIGAHOSTNO_USERNAME", - }, - { - desc: "missing GIGAHOSTNO_PASSWORD", - envVars: map[string]string{ - EnvUsername: "user", - }, - expected: "gigahostno: some credentials information are missing: GIGAHOSTNO_PASSWORD", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "gigahostno: some credentials information are missing: GIGAHOSTNO_USERNAME,GIGAHOSTNO_PASSWORD", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - username string - password string - secret string - expected string - }{ - { - desc: "success", - username: "user", - password: "secret", - secret: "super-secret", - }, - { - desc: "missing username", - password: "secret", - expected: "gigahostno: credentials missing", - }, - { - desc: "missing password", - username: "user", - expected: "gigahostno: credentials missing", - }, - { - desc: "missing credentials", - expected: "gigahostno: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.Username = test.username - config.Password = test.password - config.Secret = test.secret - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.Username = "user" - config.Password = "secret" - config.Secret = "JBSWY3DPEHPK3PXP" - - config.HTTPClient = server.Client() - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - p.client.BaseURL, _ = url.Parse(server.URL) - p.identifier.BaseURL, _ = url.Parse(server.URL) - - return p, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("POST /authenticate", - servermock.ResponseFromInternal("authenticate.json")). - Route("GET /dns/zones", - servermock.ResponseFromInternal("zones.json"), - servermock.CheckHeader(). - WithAuthorization("Bearer secrettoken")). - Route("POST /dns/zones/123/records", - servermock.ResponseFromInternal("create_record.json"), - servermock.CheckRequestJSONBodyFromInternal("create_record-request.json"), - servermock.CheckHeader(). - WithAuthorization("Bearer secrettoken")). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_Present_token_not_expired(t *testing.T) { - provider := mockBuilder(). - Route("GET /dns/zones", - servermock.ResponseFromInternal("zones.json"), - servermock.CheckHeader(). - WithAuthorization("Bearer secret-token")). - Route("POST /dns/zones/123/records", - servermock.ResponseFromInternal("create_record.json"), - servermock.CheckRequestJSONBodyFromInternal("create_record-request.json"), - servermock.CheckHeader(). - WithAuthorization("Bearer secret-token")). - Build(t) - - provider.token = &internal.Token{ - Token: "secret-token", - TokenExpire: 65322892800, // 2040-01-01 - CustomerID: "123", - } - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("POST /authenticate", - servermock.ResponseFromInternal("authenticate.json")). - Route("GET /dns/zones", - servermock.ResponseFromInternal("zones.json"), - servermock.CheckHeader(). - WithAuthorization("Bearer secrettoken")). - Route("GET /dns/zones/123/records", - servermock.ResponseFromInternal("zone_records.json"), - servermock.CheckHeader(). - WithAuthorization("Bearer secrettoken")). - Route("DELETE /dns/zones/123/records/jkl012", - servermock.ResponseFromInternal("delete_record.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "_acme-challenge"). - With("type", "TXT"), - servermock.CheckHeader(). - WithAuthorization("Bearer secrettoken")). - Build(t) - - err := provider.CleanUp("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp_token_not_expired(t *testing.T) { - provider := mockBuilder(). - Route("GET /dns/zones", - servermock.ResponseFromInternal("zones.json"), - servermock.CheckHeader(). - WithAuthorization("Bearer secret-token")). - Route("GET /dns/zones/123/records", - servermock.ResponseFromInternal("zone_records.json"), - servermock.CheckHeader(). - WithAuthorization("Bearer secret-token")). - Route("DELETE /dns/zones/123/records/jkl012", - servermock.ResponseFromInternal("delete_record.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "_acme-challenge"). - With("type", "TXT"), - servermock.CheckHeader(). - WithAuthorization("Bearer secret-token")). - Build(t) - - provider.token = &internal.Token{ - Token: "secret-token", - TokenExpire: 65322892800, // 2040-01-01 - CustomerID: "123", - } - - err := provider.CleanUp("example.com", "abc", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/gigahostno/internal/client.go b/providers/dns/gigahostno/internal/client.go deleted file mode 100644 index cfff3a7b8..000000000 --- a/providers/dns/gigahostno/internal/client.go +++ /dev/null @@ -1,172 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" -) - -const defaultBaseURL = "https://api.gigahost.no/api/v0" - -const authorizationHeader = "Authorization" - -// Client the Gigahost.no API client. -type Client struct { - BaseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient() *Client { - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - BaseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - } -} - -// GetZones returns all zones. -func (c *Client) GetZones(ctx context.Context) ([]Zone, error) { - endpoint := c.BaseURL.JoinPath("dns", "zones") - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var result APIResponse[[]Zone] - - err = c.do(ctx, req, &result) - if err != nil { - return nil, err - } - - return result.Data, nil -} - -// GetZoneRecords returns all records for a zone. -func (c *Client) GetZoneRecords(ctx context.Context, zoneID string) ([]Record, error) { - endpoint := c.BaseURL.JoinPath("dns", "zones", zoneID, "records") - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var result APIResponse[[]Record] - - err = c.do(ctx, req, &result) - if err != nil { - return nil, err - } - - return result.Data, nil -} - -// CreateNewRecord creates a new record. -func (c *Client) CreateNewRecord(ctx context.Context, zoneID string, record Record) error { - endpoint := c.BaseURL.JoinPath("dns", "zones", zoneID, "records") - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) - if err != nil { - return err - } - - return c.do(ctx, req, nil) -} - -// DeleteRecord deletes a record. -func (c *Client) DeleteRecord(ctx context.Context, zoneID, recordID, name, recordType string) error { - endpoint := c.BaseURL.JoinPath("dns", "zones", zoneID, "records", recordID) - - query := endpoint.Query() - query.Set("name", name) - query.Set("type", recordType) - endpoint.RawQuery = query.Encode() - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return err - } - - return c.do(ctx, req, nil) -} - -func (c *Client) do(ctx context.Context, req *http.Request, result any) error { - useragent.SetHeader(req.Header) - - req.Header.Set(authorizationHeader, "Bearer "+getToken(ctx)) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIError - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return &errAPI -} diff --git a/providers/dns/gigahostno/internal/client_test.go b/providers/dns/gigahostno/internal/client_test.go deleted file mode 100644 index 8d1298947..000000000 --- a/providers/dns/gigahostno/internal/client_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient() - - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(). - WithAuthorization("Bearer secret"), - ) -} - -func TestClient_GetZones(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/zones", - servermock.ResponseFromFixture("zones.json")). - Build(t) - - zones, err := client.GetZones(mockContext(t)) - require.NoError(t, err) - - expected := []Zone{ - { - ID: "123", - Name: "example.com", - NameDisplay: "example.com", - Type: "NATIVE", - Active: "1", - }, - { - ID: "226", - Name: "example.org", - NameDisplay: "example.org", - Type: "NATIVE", - Active: "1", - }, - { - ID: "229", - Name: "example.xn--zckzah", - NameDisplay: "example.テスト", - Type: "NATIVE", - Active: "1", - }, - } - - assert.Equal(t, expected, zones) -} - -func TestClient_GetZones_error(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/zones", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) - - _, err := client.GetZones(mockContext(t)) - require.EqualError(t, err, "401: 401 Unauthorized: 401 Unauthorized") -} - -func TestClient_GetZoneRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/zones/123/records", - servermock.ResponseFromFixture("zone_records.json")). - Build(t) - - zones, err := client.GetZoneRecords(mockContext(t), "123") - require.NoError(t, err) - - expected := []Record{ - { - ID: "abc123", - Name: "@", - Type: "A", - Value: "185.125.168.166", - TTL: 3600, - }, - { - ID: "def456", - Name: "www", - Type: "A", - Value: "185.125.168.166", - TTL: 3600, - }, - { - ID: "ghi789", - Name: "@", - Type: "MX", - Value: "mail.example.no", - TTL: 3600, - }, - { - ID: "jkl012", - Name: "_acme-challenge", - Type: "TXT", - Value: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: 120, - }, - } - - assert.Equal(t, expected, zones) -} - -func TestClient_CreateNewRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /dns/zones/example.com/records", - servermock.ResponseFromFixture("create_record.json"), - servermock.CheckRequestJSONBodyFromFixture("create_record-request.json")). - Build(t) - - record := Record{ - Name: "_acme-challenge", - Type: "TXT", - Value: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: 120, - } - - err := client.CreateNewRecord(mockContext(t), "example.com", record) - require.NoError(t, err) -} - -func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("/dns/zones/123/records/abc123", - servermock.ResponseFromFixture("delete_record.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "_acme-challenge"). - With("type", "TXT")). - Build(t) - - err := client.DeleteRecord(mockContext(t), "123", "abc123", "_acme-challenge", "TXT") - require.NoError(t, err) -} diff --git a/providers/dns/gigahostno/internal/fixtures/authenticate-request.json b/providers/dns/gigahostno/internal/fixtures/authenticate-request.json deleted file mode 100644 index c641cd3e5..000000000 --- a/providers/dns/gigahostno/internal/fixtures/authenticate-request.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "username": "user", - "password": "secret" -} diff --git a/providers/dns/gigahostno/internal/fixtures/authenticate.json b/providers/dns/gigahostno/internal/fixtures/authenticate.json deleted file mode 100644 index 2c43ccbfe..000000000 --- a/providers/dns/gigahostno/internal/fixtures/authenticate.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "meta": { - "status": 200, - "status_message": "200 OK", - "maintenance": false - }, - "data": { - "token": "secrettoken", - "token_expire": 1577836800, - "customer_id": "16030", - "contact_id": "15182", - "customer_name": "Cloudline AS", - "contact_username": "test@example.com", - "contact_access_level": "admin", - "customer_address": "Grønland 14", - "customer_zipcode": "5918", - "customer_city": "Frekhaug", - "customer_province": "Vestland", - "ga_secret": "ga_secret", - "ga_enabled": "1", - "vat": 1 - } -} diff --git a/providers/dns/gigahostno/internal/fixtures/create_record-request.json b/providers/dns/gigahostno/internal/fixtures/create_record-request.json deleted file mode 100644 index f8f0b5b11..000000000 --- a/providers/dns/gigahostno/internal/fixtures/create_record-request.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "record_name": "_acme-challenge", - "record_type": "TXT", - "record_value": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "record_ttl": 120 -} diff --git a/providers/dns/gigahostno/internal/fixtures/create_record.json b/providers/dns/gigahostno/internal/fixtures/create_record.json deleted file mode 100644 index 9232677d7..000000000 --- a/providers/dns/gigahostno/internal/fixtures/create_record.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "meta": { - "status": 201, - "status_message": "201 Created", - "message": "Record created successfully." - } -} diff --git a/providers/dns/gigahostno/internal/fixtures/delete_record.json b/providers/dns/gigahostno/internal/fixtures/delete_record.json deleted file mode 100644 index 9d87f2f42..000000000 --- a/providers/dns/gigahostno/internal/fixtures/delete_record.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "meta": { - "status": 200, - "status_message": "200 OK", - "message": "Record deleted successfully." - } -} diff --git a/providers/dns/gigahostno/internal/fixtures/error.json b/providers/dns/gigahostno/internal/fixtures/error.json deleted file mode 100644 index f2fcfd437..000000000 --- a/providers/dns/gigahostno/internal/fixtures/error.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "meta": { - "status": 401, - "status_message": "401 Unauthorized", - "maintenance": false, - "message": "401 Unauthorized" - }, - "data": [] -} diff --git a/providers/dns/gigahostno/internal/fixtures/zone_records.json b/providers/dns/gigahostno/internal/fixtures/zone_records.json deleted file mode 100644 index e67ff83f4..000000000 --- a/providers/dns/gigahostno/internal/fixtures/zone_records.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "meta": { - "status": 200, - "status_message": "200 OK" - }, - "data": [ - { - "record_id": "abc123", - "record_name": "@", - "record_type": "A", - "record_value": "185.125.168.166", - "record_ttl": 3600, - "record_priority": null - }, - { - "record_id": "def456", - "record_name": "www", - "record_type": "A", - "record_value": "185.125.168.166", - "record_ttl": 3600, - "record_priority": null - }, - { - "record_id": "ghi789", - "record_name": "@", - "record_type": "MX", - "record_value": "mail.example.no", - "record_ttl": 3600, - "record_priority": 10 - }, - { - "record_id": "jkl012", - "record_name": "_acme-challenge", - "record_type": "TXT", - "record_value": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "record_ttl": 120 - } - ] -} diff --git a/providers/dns/gigahostno/internal/fixtures/zones.json b/providers/dns/gigahostno/internal/fixtures/zones.json deleted file mode 100644 index d45b0ac49..000000000 --- a/providers/dns/gigahostno/internal/fixtures/zones.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "meta": { - "status": 200, - "status_message": "200 OK", - "maintenance": false, - "message": "200 OK" - }, - "data": [ - { - "zone_id": "123", - "cust_id": "16030", - "order_id": "26117", - "zone_name": "example.com", - "zone_type": "NATIVE", - "zone_active": "1", - "zone_protected": "1", - "zone_is_registered": "1", - "domain_registrar": "norid", - "domain_status": "active", - "domain_registered_date": "2025-11-23 15:17:38", - "domain_expiry_date": "2026-11-23 15:17:38", - "domain_updated_date": "2025-11-23 16:17:38", - "domain_auto_renew": "1", - "domain_epp_id": "LEG2175D-NORID", - "domain_registrant_id": "CA19777O", - "domain_tech_id": "GH295R", - "domain_auth_info": "XXXXXXXXXXXXXXX", - "domain_locked": "0", - "domain_dnssec": "0", - "domain_dnssec_data": null, - "domain_protected_email": null, - "zone_created": "2025-11-23 16:17:29", - "zone_updated": 1700000000, - "external_dns": "0", - "record_count": 4, - "zone_name_display": "example.com" - }, - { - "zone_id": "226", - "cust_id": "16030", - "order_id": "26114", - "zone_name": "example.org", - "zone_type": "NATIVE", - "zone_active": "1", - "zone_protected": "1", - "zone_is_registered": "1", - "domain_registrar": "norid", - "domain_status": "active", - "domain_registered_date": "2025-11-23 14:15:01", - "domain_expiry_date": "2026-11-23 14:15:01", - "domain_updated_date": "2025-11-23 15:15:02", - "domain_auto_renew": "1", - "domain_epp_id": "TEO218D-NORID", - "domain_registrant_id": "CA19774O", - "domain_tech_id": "GH295R", - "domain_auth_info": "XXXXXXXXXXXXXX", - "domain_locked": "0", - "domain_dnssec": "0", - "domain_dnssec_data": null, - "domain_protected_email": null, - "zone_created": "2025-11-23 15:13:27", - "zone_updated": 1700000000, - "external_dns": "0", - "record_count": 5, - "zone_name_display": "example.org" - }, - { - "zone_id": "229", - "cust_id": "16030", - "order_id": "26119", - "zone_name": "example.xn--zckzah", - "zone_type": "NATIVE", - "zone_active": "1", - "zone_protected": "1", - "zone_is_registered": "1", - "domain_registrar": "norid", - "domain_status": "active", - "domain_registered_date": "2014-12-01 12:40:48", - "domain_expiry_date": "2026-12-01 12:40:48", - "domain_updated_date": "2025-11-23 15:37:36", - "domain_auto_renew": "1", - "domain_epp_id": "DIT1003D-NORID", - "domain_registrant_id": "DCA822O", - "domain_tech_id": "GH295R", - "domain_auth_info": "XXXXXXXXXXXXXX", - "domain_locked": "0", - "domain_dnssec": "0", - "domain_dnssec_data": null, - "domain_protected_email": null, - "zone_created": "2025-11-23 16:37:15", - "zone_updated": 1700000000, - "external_dns": "0", - "record_count": 4, - "zone_name_display": "example.\u30C6\u30B9\u30C8" - } - ] -} diff --git a/providers/dns/gigahostno/internal/identity.go b/providers/dns/gigahostno/internal/identity.go deleted file mode 100644 index 262dfabdd..000000000 --- a/providers/dns/gigahostno/internal/identity.go +++ /dev/null @@ -1,122 +0,0 @@ -package internal - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" - "github.com/pquerna/otp/totp" -) - -type token string - -const tokenKey token = "token" - -type Identifier struct { - username string - password string - Secret string - - BaseURL *url.URL - HTTPClient *http.Client -} - -func NewIdentifier(username, password, secret string) (*Identifier, error) { - if username == "" || password == "" { - return nil, errors.New("credentials missing") - } - - baseURL, _ := url.Parse(defaultBaseURL) - - return &Identifier{ - username: username, - password: password, - Secret: secret, - BaseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -func (c *Identifier) Authenticate(ctx context.Context) (*Token, error) { - endpoint := c.BaseURL.JoinPath("authenticate") - - auth := Auth{Username: c.username, Password: c.password} - - if c.Secret != "" { - tan, err := totp.GenerateCode(c.Secret, time.Now()) - if err != nil { - return nil, fmt.Errorf("generate TOTP: %w", err) - } - - auth.Code, err = strconv.Atoi(tan) - if err != nil { - return nil, fmt.Errorf("parse TOTP: %w", err) - } - } - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, auth) - if err != nil { - return nil, err - } - - var result APIResponse[*Token] - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return result.Data, nil -} - -func (c *Identifier) do(req *http.Request, result any) error { - useragent.SetHeader(req.Header) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func WithContext(ctx context.Context, credential string) context.Context { - return context.WithValue(ctx, tokenKey, credential) -} - -func getToken(ctx context.Context) string { - credential, ok := ctx.Value(tokenKey).(string) - if !ok { - return "" - } - - return credential -} diff --git a/providers/dns/gigahostno/internal/identity_test.go b/providers/dns/gigahostno/internal/identity_test.go deleted file mode 100644 index 09d72746a..000000000 --- a/providers/dns/gigahostno/internal/identity_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package internal - -import ( - "context" - "net/http/httptest" - "net/url" - "testing" - "time" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func setupIdentifierClient(server *httptest.Server) (*Identifier, error) { - client, err := NewIdentifier("user", "secret", "") - if err != nil { - return nil, err - } - - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil -} - -func mockContext(t *testing.T) context.Context { - t.Helper() - - return context.WithValue(t.Context(), tokenKey, "secret") -} - -func TestIdentifier_Authenticate(t *testing.T) { - identifier := servermock.NewBuilder[*Identifier](setupIdentifierClient). - Route("POST /authenticate", - servermock.ResponseFromFixture("authenticate.json"), - servermock.CheckRequestJSONBodyFromFixture("authenticate-request.json")). - Build(t) - - token, err := identifier.Authenticate(context.Background()) - require.NoError(t, err) - - expected := &Token{ - Token: "secrettoken", - TokenExpire: 1577836800, - CustomerID: "16030", - ContactID: "15182", - CustomerName: "Cloudline AS", - ContactUsername: "test@example.com", - ContactAccessLevel: "admin", - CustomerAddress: "Grønland 14", - CustomerZipcode: "5918", - CustomerCity: "Frekhaug", - CustomerProvince: "Vestland", - GASecret: "ga_secret", - GAEnabled: "1", - VAT: 1, - } - - assert.Equal(t, expected, token) -} - -func TestToken_IsExpired(t *testing.T) { - testCases := []struct { - desc string - token *Token - assert assert.BoolAssertionFunc - }{ - { - desc: "nil", - assert: assert.True, - }, - { - desc: "empty", - token: &Token{}, - assert: assert.True, - }, - { - desc: "not expired", - token: &Token{ - TokenExpire: 65322892800, // 2040-01-01 - }, - assert: assert.False, - }, - { - desc: "now", - token: &Token{ - TokenExpire: time.Now().Unix(), - }, - assert: assert.True, - }, - { - desc: "now + 2 minutes", - token: &Token{ - TokenExpire: time.Now().Add(2 * time.Minute).Unix(), - }, - assert: assert.False, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - test.assert(t, test.token.IsExpired()) - }) - } -} diff --git a/providers/dns/gigahostno/internal/types.go b/providers/dns/gigahostno/internal/types.go deleted file mode 100644 index e998dc084..000000000 --- a/providers/dns/gigahostno/internal/types.go +++ /dev/null @@ -1,73 +0,0 @@ -package internal - -import ( - "fmt" - "time" -) - -type APIError struct { - Meta MetaData `json:"meta"` -} - -func (a *APIError) Error() string { - return fmt.Sprintf("%d: %s: %s", a.Meta.Status, a.Meta.StatusMessage, a.Meta.Message) -} - -type MetaData struct { - Status int `json:"status,omitempty"` - StatusMessage string `json:"status_message,omitempty"` - Maintenance bool `json:"maintenance"` - Message string `json:"message,omitempty"` -} - -type APIResponse[T any] struct { - Meta MetaData `json:"meta"` - Data T `json:"data,omitempty"` -} - -type Zone struct { - ID string `json:"zone_id,omitempty"` - Name string `json:"zone_name,omitempty"` - NameDisplay string `json:"zone_name_display,omitempty"` - Type string `json:"zone_type,omitempty"` - Active string `json:"zone_active,omitempty"` -} - -type Record struct { - ID string `json:"record_id,omitempty"` - Name string `json:"record_name,omitempty"` - Type string `json:"record_type,omitempty"` - Value string `json:"record_value,omitempty"` - TTL int `json:"record_ttl,omitempty"` -} - -type Auth struct { - Username string `json:"username"` - Password string `json:"password"` - Code int `json:"code,omitempty"` -} - -type Token struct { - Token string `json:"token,omitempty"` - TokenExpire int64 `json:"token_expire,omitempty"` - CustomerID string `json:"customer_id,omitempty"` - ContactID string `json:"contact_id,omitempty"` - CustomerName string `json:"customer_name,omitempty"` - ContactUsername string `json:"contact_username,omitempty"` - ContactAccessLevel string `json:"contact_access_level,omitempty"` - CustomerAddress string `json:"customer_address,omitempty"` - CustomerZipcode string `json:"customer_zipcode,omitempty"` - CustomerCity string `json:"customer_city,omitempty"` - CustomerProvince string `json:"customer_province,omitempty"` - GASecret string `json:"ga_secret,omitempty"` - GAEnabled string `json:"ga_enabled,omitempty"` - VAT int `json:"vat,omitempty"` -} - -func (t *Token) IsExpired() bool { - if t == nil { - return true - } - - return time.Now().UTC().Add(1 * time.Minute).After(time.Unix(t.TokenExpire, 0).UTC()) -} diff --git a/providers/dns/glesys/glesys.go b/providers/dns/glesys/glesys.go index 729756235..c25b693c5 100644 --- a/providers/dns/glesys/glesys.go +++ b/providers/dns/glesys/glesys.go @@ -9,13 +9,13 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/glesys/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) +const minTTL = 60 + // Environment variables names. const ( envNamespace = "GLESYS_" @@ -29,10 +29,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const minTTL = 60 - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIUser string @@ -100,8 +96,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, client: client, @@ -136,7 +130,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // save data necessary for CleanUp d.activeRecords[info.EffectiveFQDN] = recordID - return nil } @@ -147,7 +140,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { // acquire lock and retrieve authZone d.inProgressMu.Lock() defer d.inProgressMu.Unlock() - if _, ok := d.activeRecords[info.EffectiveFQDN]; !ok { // if there is no cleanup information then just return return nil diff --git a/providers/dns/glesys/glesys.toml b/providers/dns/glesys/glesys.toml index c0e2613b8..10c3e0732 100644 --- a/providers/dns/glesys/glesys.toml +++ b/providers/dns/glesys/glesys.toml @@ -7,7 +7,7 @@ Since = "v0.5.0" Example = ''' GLESYS_API_USER=xxxxx \ GLESYS_API_KEY=yyyyy \ -lego --dns glesys -d '*.example.com' -d example.com run +lego --email you@example.com --dns glesys --domains my.example.org run ''' [Configuration] @@ -15,10 +15,10 @@ lego --dns glesys -d '*.example.com' -d example.com run GLESYS_API_USER = "API user" GLESYS_API_KEY = "API key" [Configuration.Additional] - GLESYS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 20)" - GLESYS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 1200)" - GLESYS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" - GLESYS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 10)" + GLESYS_POLLING_INTERVAL = "Time between DNS propagation check" + GLESYS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + GLESYS_TTL = "The TTL of the TXT record used for the DNS challenge" + GLESYS_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://github.com/GleSYS/API/wiki/API-Documentation" diff --git a/providers/dns/glesys/glesys_test.go b/providers/dns/glesys/glesys_test.go index f2d65e514..d5fdf36da 100644 --- a/providers/dns/glesys/glesys_test.go +++ b/providers/dns/glesys/glesys_test.go @@ -56,7 +56,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -131,7 +130,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -145,7 +143,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/glesys/internal/client.go b/providers/dns/glesys/internal/client.go index ee6ebc058..038c6f0d5 100644 --- a/providers/dns/glesys/internal/client.go +++ b/providers/dns/glesys/internal/client.go @@ -24,7 +24,7 @@ type Client struct { HTTPClient *http.Client } -func NewClient(apiUser, apiKey string) *Client { +func NewClient(apiUser string, apiKey string) *Client { baseURL, _ := url.Parse(defaultBaseURL) return &Client{ @@ -102,7 +102,6 @@ func (c *Client) do(req *http.Request) (*apiResponse, error) { } var response apiResponse - err = json.Unmarshal(raw, &response) if err != nil { return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) diff --git a/providers/dns/glesys/internal/client_test.go b/providers/dns/glesys/internal/client_test.go index cd71757ff..7e8ca9724 100644 --- a/providers/dns/glesys/internal/client_test.go +++ b/providers/dns/glesys/internal/client_test.go @@ -1,49 +1,79 @@ package internal import ( + "context" + "fmt" + "io" + "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("user", "secret") - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) +func setupTest(t *testing.T, method, pattern string, status int, file string) *Client { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithBasicAuth("user", "secret"), - ) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest) + return + } + + apiUser, apiKey, ok := req.BasicAuth() + if apiUser != "user" || apiKey != "secret" || !ok { + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + if file == "" { + rw.WriteHeader(status) + return + } + + open, err := os.Open(filepath.Join("fixtures", file)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = open.Close() }() + + rw.WriteHeader(status) + _, err = io.Copy(rw, open) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + client := NewClient("user", "secret") + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client } func TestClient_AddTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /domain/addrecord", - servermock.ResponseFromFixture("add-record.json"), - servermock.CheckRequestJSONBody(`{"domainname":"example.com","host":"foo","type":"TXT","data":"txt","ttl":120}`)). - Build(t) + client := setupTest(t, http.MethodPost, "/domain/addrecord", http.StatusOK, "add-record.json") - recordID, err := client.AddTXTRecord(t.Context(), "example.com", "foo", "txt", 120) + recordID, err := client.AddTXTRecord(context.Background(), "example.com", "foo", "txt", 120) require.NoError(t, err) assert.Equal(t, 123, recordID) } func TestClient_DeleteTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /domain/deleterecord", - servermock.ResponseFromFixture("delete-record.json"), - servermock.CheckRequestJSONBody(`{"recordid":123}`)). - Build(t) + client := setupTest(t, http.MethodPost, "/domain/deleterecord", http.StatusOK, "delete-record.json") - err := client.DeleteTXTRecord(t.Context(), 123) + err := client.DeleteTXTRecord(context.Background(), 123) require.NoError(t, err) } diff --git a/providers/dns/godaddy/godaddy.go b/providers/dns/godaddy/godaddy.go index 1603bb57e..f872f217e 100644 --- a/providers/dns/godaddy/godaddy.go +++ b/providers/dns/godaddy/godaddy.go @@ -8,13 +8,13 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/godaddy/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) +const minTTL = 600 + // Environment variables names. const ( envNamespace = "GODADDY_" @@ -28,10 +28,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const minTTL = 600 - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string @@ -47,7 +43,7 @@ func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, @@ -96,8 +92,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{config: config, client: client}, nil } @@ -125,14 +119,13 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { ctx := context.Background() - existingRecords, err := d.client.GetRecords(ctx, authZone, "TXT", subDomain) + records, err := d.client.GetRecords(ctx, authZone, "TXT", subDomain) if err != nil { return fmt.Errorf("godaddy: failed to get TXT records: %w", err) } var newRecords []internal.DNSRecord - - for _, record := range existingRecords { + for _, record := range records { if record.Data != "" { newRecords = append(newRecords, record) } @@ -172,29 +165,34 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { ctx := context.Background() - existingRecords, err := d.client.GetRecords(ctx, authZone, "TXT", subDomain) + records, err := d.client.GetRecords(ctx, authZone, "TXT", subDomain) + if err != nil { + return fmt.Errorf("godaddy: failed to get TXT records: %w", err) + } + + if len(records) == 0 { + return nil + } + + allTxtRecords, err := d.client.GetRecords(ctx, authZone, "TXT", "") if err != nil { return fmt.Errorf("godaddy: failed to get all TXT records: %w", err) } - var recordsToKeep []internal.DNSRecord - - for _, record := range existingRecords { + var recordsKeep []internal.DNSRecord + for _, record := range allTxtRecords { if record.Data != info.Value && record.Data != "" { - recordsToKeep = append(recordsToKeep, record) + recordsKeep = append(recordsKeep, record) } } - if len(recordsToKeep) == 0 { - err = d.client.DeleteTxtRecords(ctx, authZone, subDomain) - if err != nil { - return fmt.Errorf("godaddy: failed to delete TXT record: %w", err) - } - - return nil + // GoDaddy API don't provide a way to delete a record, an "empty" record must be added. + if len(recordsKeep) == 0 { + emptyRecord := internal.DNSRecord{Name: "empty", Data: ""} + recordsKeep = append(recordsKeep, emptyRecord) } - err = d.client.UpdateTxtRecords(ctx, recordsToKeep, authZone, subDomain) + err = d.client.UpdateTxtRecords(ctx, recordsKeep, authZone, "") if err != nil { return fmt.Errorf("godaddy: failed to remove TXT record: %w", err) } diff --git a/providers/dns/godaddy/godaddy.toml b/providers/dns/godaddy/godaddy.toml index b906605b3..5983b0c09 100644 --- a/providers/dns/godaddy/godaddy.toml +++ b/providers/dns/godaddy/godaddy.toml @@ -7,7 +7,7 @@ Since = "v0.5.0" Example = ''' GODADDY_API_KEY=xxxxxxxx \ GODADDY_API_SECRET=yyyyyyyy \ -lego --dns godaddy -d '*.example.com' -d example.com run +lego --email you@example.com --dns godaddy --domains my.example.org run ''' Additional = ''' @@ -24,10 +24,10 @@ https://community.letsencrypt.org/t/getting-unauthorized-url-error-while-trying- GODADDY_API_KEY = "API key" GODADDY_API_SECRET = "API secret" [Configuration.Additional] - GODADDY_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - GODADDY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - GODADDY_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)" - GODADDY_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + GODADDY_POLLING_INTERVAL = "Time between DNS propagation check" + GODADDY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + GODADDY_TTL = "The TTL of the TXT record used for the DNS challenge" + GODADDY_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://developer.godaddy.com/doc/endpoint/domains" diff --git a/providers/dns/godaddy/godaddy_test.go b/providers/dns/godaddy/godaddy_test.go index 38b39672e..4cb5f2721 100644 --- a/providers/dns/godaddy/godaddy_test.go +++ b/providers/dns/godaddy/godaddy_test.go @@ -56,7 +56,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -127,7 +126,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -141,7 +139,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/godaddy/internal/client.go b/providers/dns/godaddy/internal/client.go index 9dd337ddc..64f9f0bf7 100644 --- a/providers/dns/godaddy/internal/client.go +++ b/providers/dns/godaddy/internal/client.go @@ -26,7 +26,7 @@ type Client struct { HTTPClient *http.Client } -func NewClient(apiKey, apiSecret string) *Client { +func NewClient(apiKey string, apiSecret string) *Client { baseURL, _ := url.Parse(DefaultBaseURL) return &Client{ @@ -37,8 +37,6 @@ func NewClient(apiKey, apiSecret string) *Client { } } -// GetRecords retrieves DNS Records for the specified Domain. -// https://developer.godaddy.com/doc/endpoint/domains#/v1/recordGet func (c *Client) GetRecords(ctx context.Context, domainZone, rType, recordName string) ([]DNSRecord, error) { endpoint := c.baseURL.JoinPath("v1", "domains", domainZone, "records", rType, recordName) @@ -48,7 +46,6 @@ func (c *Client) GetRecords(ctx context.Context, domainZone, rType, recordName s } var records []DNSRecord - err = c.do(req, &records) if err != nil { return nil, err @@ -57,8 +54,6 @@ func (c *Client) GetRecords(ctx context.Context, domainZone, rType, recordName s return records, nil } -// UpdateTxtRecords replaces all DNS Records for the specified Domain with the specified Type. -// https://developer.godaddy.com/doc/endpoint/domains#/v1/recordReplaceType func (c *Client) UpdateTxtRecords(ctx context.Context, records []DNSRecord, domainZone, recordName string) error { endpoint := c.baseURL.JoinPath("v1", "domains", domainZone, "records", "TXT", recordName) @@ -70,19 +65,6 @@ func (c *Client) UpdateTxtRecords(ctx context.Context, records []DNSRecord, doma return c.do(req, nil) } -// DeleteTxtRecords deletes all DNS Records for the specified Domain with the specified Type and Name. -// https://developer.godaddy.com/doc/endpoint/domains#/v1/recordDeleteTypeName -func (c *Client) DeleteTxtRecords(ctx context.Context, domainZone, recordName string) error { - endpoint := c.baseURL.JoinPath("v1", "domains", domainZone, "records", "TXT", recordName) - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return err - } - - return c.do(req, nil) -} - func (c *Client) do(req *http.Request, result any) error { req.Header.Set(authorizationHeader, fmt.Sprintf("sso-key %s:%s", c.apiKey, c.apiSecret)) @@ -93,8 +75,8 @@ func (c *Client) do(req *http.Request, result any) error { defer func() { _ = resp.Body.Close() }() - if resp.StatusCode/100 != 2 { - return parseError(req, resp) + if resp.StatusCode != http.StatusOK { + return errutils.NewUnexpectedResponseStatusCodeError(req, resp) } if result == nil { @@ -137,16 +119,3 @@ func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, paylo return req, nil } - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIError - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return fmt.Errorf("[status code: %d] %w", resp.StatusCode, &errAPI) -} diff --git a/providers/dns/godaddy/internal/client_test.go b/providers/dns/godaddy/internal/client_test.go index 694a16565..ccbab16d3 100644 --- a/providers/dns/godaddy/internal/client_test.go +++ b/providers/dns/godaddy/internal/client_test.go @@ -1,35 +1,40 @@ package internal import ( + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("key", "secret") - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("sso-key key:secret")) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient("key", "secret") + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client, mux } func TestClient_GetRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /v1/domains/example.com/records/TXT/", servermock.ResponseFromFixture("getrecords.json")). - Build(t) + client, mux := setupTest(t) - records, err := client.GetRecords(t.Context(), "example.com", "TXT", "") + mux.HandleFunc("/v1/domains/example.com/records/TXT/", testHandler(http.MethodGet, http.StatusOK, "getrecords.json")) + + records, err := client.GetRecords(context.Background(), "example.com", "TXT", "") require.NoError(t, err) expected := []DNSRecord{ @@ -45,21 +50,30 @@ func TestClient_GetRecords(t *testing.T) { } func TestClient_GetRecords_errors(t *testing.T) { - client := mockBuilder(). - Route("GET /v1/domains/example.com/records/TXT/", - servermock.ResponseFromFixture("errors.json").WithStatusCode(http.StatusUnprocessableEntity)). - Build(t) + client, mux := setupTest(t) - records, err := client.GetRecords(t.Context(), "example.com", "TXT", "") - require.EqualError(t, err, "[status code: 422] INVALID_BODY: Request body doesn't fulfill schema, see details in `fields`") + mux.HandleFunc("/v1/domains/example.com/records/TXT/", testHandler(http.MethodGet, http.StatusUnprocessableEntity, "errors.json")) + + records, err := client.GetRecords(context.Background(), "example.com", "TXT", "") + require.Error(t, err) assert.Nil(t, records) } func TestClient_UpdateTxtRecords(t *testing.T) { - client := mockBuilder(). - Route("PUT /v1/domains/example.com/records/TXT/lego", nil, - servermock.CheckRequestJSONBodyFromFixture("update_records-request.json")). - Build(t) + client, mux := setupTest(t) + + mux.HandleFunc("/v1/domains/example.com/records/TXT/lego", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPut { + http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed) + return + } + + auth := req.Header.Get(authorizationHeader) + if auth != "sso-key key:secret" { + http.Error(rw, fmt.Sprintf("invalid API key or secret: %s", auth), http.StatusUnauthorized) + return + } + }) records := []DNSRecord{ {Name: "_acme-challenge", Type: "TXT", Data: " ", TTL: 600}, @@ -70,16 +84,15 @@ func TestClient_UpdateTxtRecords(t *testing.T) { {Name: "_acme-challenge.lego", Type: "TXT", Data: "acme", TTL: 600}, } - err := client.UpdateTxtRecords(t.Context(), records, "example.com", "lego") + err := client.UpdateTxtRecords(context.Background(), records, "example.com", "lego") require.NoError(t, err) } func TestClient_UpdateTxtRecords_errors(t *testing.T) { - client := mockBuilder(). - Route("PUT /v1/domains/example.com/records/TXT/lego", - servermock.ResponseFromFixture("errors.json").WithStatusCode(http.StatusUnprocessableEntity), - servermock.CheckRequestJSONBodyFromFixture("update_records-request.json")). - Build(t) + client, mux := setupTest(t) + + mux.HandleFunc("/v1/domains/example.com/records/TXT/lego", + testHandler(http.MethodPut, http.StatusUnprocessableEntity, "errors.json")) records := []DNSRecord{ {Name: "_acme-challenge", Type: "TXT", Data: " ", TTL: 600}, @@ -90,26 +103,41 @@ func TestClient_UpdateTxtRecords_errors(t *testing.T) { {Name: "_acme-challenge.lego", Type: "TXT", Data: "acme", TTL: 600}, } - err := client.UpdateTxtRecords(t.Context(), records, "example.com", "lego") - require.EqualError(t, err, "[status code: 422] INVALID_BODY: Request body doesn't fulfill schema, see details in `fields`") + err := client.UpdateTxtRecords(context.Background(), records, "example.com", "lego") + require.Error(t, err) } -func TestClient_DeleteTxtRecords(t *testing.T) { - client := mockBuilder(). - Route("DELETE /v1/domains/example.com/records/TXT/foo", - servermock.Noop().WithStatusCode(http.StatusNoContent)). - Build(t) +func testHandler(method string, statusCode int, filename string) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed) + return + } - err := client.DeleteTxtRecords(t.Context(), "example.com", "foo") - require.NoError(t, err) -} - -func TestClient_DeleteTxtRecords_errors(t *testing.T) { - client := mockBuilder(). - Route("DELETE /v1/domains/example.com/records/TXT/foo", - servermock.ResponseFromFixture("error-extended.json").WithStatusCode(http.StatusConflict)). - Build(t) - - err := client.DeleteTxtRecords(t.Context(), "example.com", "foo") - require.EqualError(t, err, "[status code: 409] ACCESS_DENIED: Authenticated user is not allowed access [test: content (path=/foo) (pathRelated=/bar)]") + auth := req.Header.Get(authorizationHeader) + if auth != "sso-key key:secret" { + http.Error(rw, fmt.Sprintf("invalid API key or secret: %s", auth), http.StatusUnauthorized) + return + } + + rw.WriteHeader(statusCode) + + if statusCode == http.StatusNoContent { + return + } + + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError) + return + } + + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError) + return + } + } } diff --git a/providers/dns/godaddy/internal/fixtures/error-extended.json b/providers/dns/godaddy/internal/fixtures/error-extended.json deleted file mode 100644 index 29e5e542a..000000000 --- a/providers/dns/godaddy/internal/fixtures/error-extended.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "code": "ACCESS_DENIED", - "fields": [ - { - "code": "test", - "message": "content", - "path": "/foo", - "pathRelated": "/bar" - } - ], - "message": "Authenticated user is not allowed access" -} diff --git a/providers/dns/godaddy/internal/fixtures/update_records-request.json b/providers/dns/godaddy/internal/fixtures/update_records-request.json deleted file mode 100644 index 969afb2dc..000000000 --- a/providers/dns/godaddy/internal/fixtures/update_records-request.json +++ /dev/null @@ -1,38 +0,0 @@ -[ - { - "name": "_acme-challenge", - "type": "TXT", - "data": " ", - "ttl": 600 - }, - { - "name": "_acme-challenge.example", - "type": "TXT", - "data": "6rrai7-jm7l3PxL4WGmGoS6VMeefSHx24r-qCvUIOxU", - "ttl": 600 - }, - { - "name": "_acme-challenge.example", - "type": "TXT", - "data": "8Axt-PXQvjOVD2oi2YXqyyn8U5CDcC8P-BphlCxk3Ek", - "ttl": 600 - }, - { - "name": "_acme-challenge.lego", - "type": "TXT", - "data": " ", - "ttl": 600 - }, - { - "name": "_acme-challenge.lego", - "type": "TXT", - "data": "0Ad60wO_yxxJPFPb1deir6lQ37FPLeA02YCobo7Qm8A", - "ttl": 600 - }, - { - "name": "_acme-challenge.lego", - "type": "TXT", - "data": "acme", - "ttl": 600 - } -] diff --git a/providers/dns/godaddy/internal/types.go b/providers/dns/godaddy/internal/types.go index 3bd5c9560..fc06cda04 100644 --- a/providers/dns/godaddy/internal/types.go +++ b/providers/dns/godaddy/internal/types.go @@ -1,10 +1,5 @@ package internal -import ( - "fmt" - "strings" -) - // DNSRecord a DNS record. type DNSRecord struct { Name string `json:"name,omitempty"` @@ -18,45 +13,3 @@ type DNSRecord struct { Service string `json:"service,omitempty"` Weight int `json:"weight,omitempty"` } - -type APIError struct { - Code string `json:"code,omitempty"` - Fields []Field `json:"fields,omitempty"` - Message string `json:"message,omitempty"` -} - -func (a APIError) Error() string { - msg := new(strings.Builder) - - _, _ = fmt.Fprintf(msg, "%s: %s", a.Code, a.Message) - - for _, field := range a.Fields { - msg.WriteString(" ") - msg.WriteString(field.String()) - } - - return msg.String() -} - -type Field struct { - Code string `json:"code,omitempty"` - Message string `json:"message,omitempty"` - Path string `json:"path,omitempty"` - PathRelated string `json:"pathRelated,omitempty"` -} - -func (f Field) String() string { - msg := fmt.Sprintf("[%s: %s", f.Code, f.Message) - - if f.Path != "" { - msg += fmt.Sprintf(" (path=%s)", f.Path) - } - - if f.PathRelated != "" { - msg += fmt.Sprintf(" (pathRelated=%s)", f.PathRelated) - } - - msg += "]" - - return msg -} diff --git a/providers/dns/googledomains/googledomains.go b/providers/dns/googledomains/googledomains.go index b5eed0b03..a87895c60 100644 --- a/providers/dns/googledomains/googledomains.go +++ b/providers/dns/googledomains/googledomains.go @@ -2,12 +2,17 @@ package googledomains import ( + "context" "errors" + "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "google.golang.org/api/acmedns/v1" + "google.golang.org/api/option" ) // Environment variables names. @@ -20,7 +25,8 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) +// static compile-time check on interface implementation. +var _ challenge.Provider = &DNSProvider{} // Config is used to configure the creation of the DNSProvider. type Config struct { @@ -32,29 +38,103 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { - return &Config{} + return &Config{ + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } } -type DNSProvider struct{} - // NewDNSProvider returns the Google Domains DNS provider with a default configuration. func NewDNSProvider() (*DNSProvider, error) { - return NewDNSProviderConfig(&Config{}) + values, err := env.Get(EnvAccessToken) + if err != nil { + return nil, fmt.Errorf("googledomains: %w", err) + } + + config := NewDefaultConfig() + config.AccessToken = values[EnvAccessToken] + + return NewDNSProviderConfig(config) } // NewDNSProviderConfig returns the Google Domains DNS provider with the provided config. -func NewDNSProviderConfig(_ *Config) (*DNSProvider, error) { - return nil, errors.New("googledomains: provider has shut down") +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("googledomains: the configuration of the DNS provider is nil") + } + + if config.AccessToken == "" { + return nil, errors.New("googledomains: access token is missing") + } + + service, err := acmedns.NewService(context.Background(), option.WithHTTPClient(config.HTTPClient)) + if err != nil { + return nil, fmt.Errorf("googledomains: error creating acme dns service: %w", err) + } + + return &DNSProvider{ + config: config, + acmedns: service, + }, nil } -func (d *DNSProvider) Present(_, _, _ string) error { +type DNSProvider struct { + config *Config + acmedns *acmedns.Service +} + +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + zone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) + if err != nil { + return fmt.Errorf("googledomains: could not find zone for domain %q: %w", domain, err) + } + + rotateReq := acmedns.RotateChallengesRequest{ + AccessToken: d.config.AccessToken, + RecordsToAdd: []*acmedns.AcmeTxtRecord{getAcmeTxtRecord(domain, keyAuth)}, + KeepExpiredRecords: false, + } + + call := d.acmedns.AcmeChallengeSets.RotateChallenges(zone, &rotateReq) + _, err = call.Do() + if err != nil { + return fmt.Errorf("googledomains: error adding challenge for domain %s: %w", domain, err) + } return nil } -func (d *DNSProvider) CleanUp(_, _, _ string) error { +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + zone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) + if err != nil { + return fmt.Errorf("googledomains: could not find zone for domain %q: %w", domain, err) + } + + rotateReq := acmedns.RotateChallengesRequest{ + AccessToken: d.config.AccessToken, + RecordsToRemove: []*acmedns.AcmeTxtRecord{getAcmeTxtRecord(domain, keyAuth)}, + KeepExpiredRecords: false, + } + + call := d.acmedns.AcmeChallengeSets.RotateChallenges(zone, &rotateReq) + _, err = call.Do() + if err != nil { + return fmt.Errorf("googledomains: error cleaning up challenge for domain %s: %w", domain, err) + } return nil } func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return dns01.DefaultPropagationTimeout, dns01.DefaultPollingInterval + return d.config.PropagationTimeout, d.config.PollingInterval +} + +func getAcmeTxtRecord(domain, keyAuth string) *acmedns.AcmeTxtRecord { + challengeInfo := dns01.GetChallengeInfo(domain, keyAuth) + + return &acmedns.AcmeTxtRecord{ + Fqdn: challengeInfo.EffectiveFQDN, + Digest: challengeInfo.Value, + } } diff --git a/providers/dns/googledomains/googledomains.toml b/providers/dns/googledomains/googledomains.toml index 52330795d..2b1c4dddd 100644 --- a/providers/dns/googledomains/googledomains.toml +++ b/providers/dns/googledomains/googledomains.toml @@ -1,23 +1,21 @@ Name = "Google Domains" -Description = ''' -The Google Domains DNS provider has shut down. -''' -URL = "https://github.com/go-acme/lego/issues/2553" +Description = '''''' +URL = "https://domains.google" Code = "googledomains" Since = "v4.11.0" Example = ''' GOOGLE_DOMAINS_ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --dns googledomains -d '*.example.com' -d example.com run +lego --email you@example.com --dns googledomains --domains my.example.org run ''' [Configuration] [Configuration.Credentials] GOOGLE_DOMAINS_ACCESS_TOKEN = "Access token" [Configuration.Additional] - GOOGLE_DOMAINS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - GOOGLE_DOMAINS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - GOOGLE_DOMAINS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + GOOGLE_DOMAINS_POLLING_INTERVAL = "Time between DNS propagation check" + GOOGLE_DOMAINS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + GOOGLE_DOMAINS_HTTP_TIMEOUT = "API request timeout" [Links] GoClient = "https://github.com/googleapis/google-api-go-client" diff --git a/providers/dns/myaddr/myaddr_test.go b/providers/dns/googledomains/googledomains_test.go similarity index 76% rename from providers/dns/myaddr/myaddr_test.go rename to providers/dns/googledomains/googledomains_test.go index 8e555ecfd..038fb5346 100644 --- a/providers/dns/myaddr/myaddr_test.go +++ b/providers/dns/googledomains/googledomains_test.go @@ -1,4 +1,4 @@ -package myaddr +package googledomains import ( "testing" @@ -9,7 +9,9 @@ import ( const envDomain = envNamespace + "DOMAIN" -var envTest = tester.NewEnvTest(EnvPrivateKeysMapping).WithDomain(envDomain) +var envTest = tester.NewEnvTest(EnvAccessToken). + WithDomain(envDomain). + WithLiveTestRequirements(EnvAccessToken, envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { @@ -20,33 +22,29 @@ func TestNewDNSProvider(t *testing.T) { { desc: "success", envVars: map[string]string{ - EnvPrivateKeysMapping: "example:123", + EnvAccessToken: "abc", }, + expected: "", }, { desc: "missing credentials", envVars: map[string]string{}, - expected: "myaddr: some credentials information are missing: MYADDR_PRIVATE_KEYS_MAPPING", + expected: "googledomains: some credentials information are missing: GOOGLE_DOMAINS_ACCESS_TOKEN", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() - envTest.Apply(test.envVars) - p, err := NewDNSProvider() - if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) } else { - require.EqualError(t, err, test.expected) + require.Error(t, err) + require.Contains(t, err.Error(), test.expected) } }) } @@ -55,23 +53,23 @@ func TestNewDNSProvider(t *testing.T) { func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string - credentials map[string]string + accessToken string expected string }{ { desc: "success", - credentials: map[string]string{"example": "123"}, + accessToken: "abc", }, { desc: "missing credentials", - expected: "myaddr: credentials missing", + expected: "googledomains: access token is missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() - config.Credentials = test.credentials + config.AccessToken = test.accessToken p, err := NewDNSProviderConfig(config) @@ -79,7 +77,6 @@ func TestNewDNSProviderConfig(t *testing.T) { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) - require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } @@ -93,7 +90,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -107,7 +103,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/gravity/gravity.go b/providers/dns/gravity/gravity.go deleted file mode 100644 index b0bbb2fcb..000000000 --- a/providers/dns/gravity/gravity.go +++ /dev/null @@ -1,209 +0,0 @@ -// Package gravity implements a DNS provider for solving the DNS-01 challenge using Gravity. -package gravity - -import ( - "context" - "errors" - "fmt" - "net/http" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/gravity/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/google/uuid" -) - -// Environment variables names. -const ( - envNamespace = "GRAVITY_" - - EnvUsername = envNamespace + "USERNAME" - EnvPassword = envNamespace + "PASSWORD" - EnvServerURL = envNamespace + "SERVER_URL" - - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" - EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - Username string - Password string - ServerURL string - - PropagationTimeout time.Duration - PollingInterval time.Duration - SequenceInterval time.Duration - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, 1*time.Second), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client - - records map[string]internal.Record - recordsMu sync.Mutex -} - -// NewDNSProvider returns a DNSProvider instance configured for Gravity. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvUsername, EnvPassword, EnvServerURL) - if err != nil { - return nil, fmt.Errorf("gravity: %w", err) - } - - config := NewDefaultConfig() - config.Username = values[EnvUsername] - config.Password = values[EnvPassword] - config.ServerURL = values[EnvServerURL] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Gravity. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("gravity: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.ServerURL, config.Username, config.Password) - if err != nil { - return nil, fmt.Errorf("gravity: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - records: make(map[string]internal.Record), - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - _, err := d.client.Login(ctx) - if err != nil { - return fmt.Errorf("gravity: login: %w", err) - } - - zone, err := d.findZone(ctx, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("gravity: %w", err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone) - if err != nil { - return fmt.Errorf("gravity: %w", err) - } - - id := uuid.New() - - record := internal.Record{ - Data: info.Value, - Hostname: subDomain, - Type: "TXT", - UID: id.String(), - } - - err = d.client.CreateDNSRecord(ctx, zone, record) - if err != nil { - return fmt.Errorf("gravity: create DNS record: %w", err) - } - - d.recordsMu.Lock() - - record.Fqdn = zone - d.records[token] = record - d.recordsMu.Unlock() - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - d.recordsMu.Lock() - record, ok := d.records[token] - d.recordsMu.Unlock() - - if !ok { - return fmt.Errorf("gravity: unknown record for '%s' '%s'", info.EffectiveFQDN, token) - } - - err := d.client.DeleteDNSRecord(context.Background(), record.Fqdn, record) - if err != nil { - return fmt.Errorf("gravity: delete record: %w", err) - } - - d.recordsMu.Lock() - delete(d.records, token) - d.recordsMu.Unlock() - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -// Sequential implements the [dns01.sequential] interface. -// It changes the behavior of the provider to resolve DNS challenges sequentially. -// Returns the interval between each iteration. -// -// Gravity supports adding multiple records for the same domain, but the DNS server doesn't work as expected: -// if you call the DNS server, it will answer only the latest record instead of all of them. -func (d *DNSProvider) Sequential() time.Duration { - return d.config.SequenceInterval -} - -func (d *DNSProvider) findZone(ctx context.Context, effectiveFQDN string) (string, error) { - var zone string - - for fqdn := range dns01.DomainsSeq(effectiveFQDN) { - zones, err := d.client.GetDNSZones(ctx, fqdn) - if err != nil { - return "", fmt.Errorf("get DNS zones: %w", err) - } - - if len(zones) != 0 { - zone = zones[0].Name - break - } - } - - if zone == "" { - return "", fmt.Errorf("could not find zone for %q", effectiveFQDN) - } - - return zone, nil -} diff --git a/providers/dns/gravity/gravity.toml b/providers/dns/gravity/gravity.toml deleted file mode 100644 index 87a303839..000000000 --- a/providers/dns/gravity/gravity.toml +++ /dev/null @@ -1,26 +0,0 @@ -Name = "Gravity" -Description = '''''' -URL = "https://gravity.beryju.io/" -Code = "gravity" -Since = "v4.30.0" - -Example = ''' -GRAVITY_SERVER_URL="https://example.org:1234" \ -GRAVITY_USERNAME="xxxxxxxxxxxxxxxxxxxxx" \ -GRAVITY_PASSWORD="yyyyyyyyyyyyyyyyyyyyy" \ -lego --dns gravity -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - GRAVITY_SERVER_URL = "URL of the server" - GRAVITY_USERNAME = "Username" - GRAVITY_PASSWORD = "Password" - [Configuration.Additional] - GRAVITY_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - GRAVITY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - GRAVITY_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 1)" - GRAVITY_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://gravity.beryju.io/docs/api/reference/" diff --git a/providers/dns/gravity/gravity_test.go b/providers/dns/gravity/gravity_test.go deleted file mode 100644 index b59b856fe..000000000 --- a/providers/dns/gravity/gravity_test.go +++ /dev/null @@ -1,254 +0,0 @@ -package gravity - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/go-acme/lego/v4/providers/dns/gravity/internal" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest( - EnvUsername, - EnvPassword, - EnvServerURL, -).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvUsername: "user", - EnvPassword: "secret", - EnvServerURL: "https://example.org:1234", - }, - }, - { - desc: "missing EnvUsername", - envVars: map[string]string{ - EnvUsername: "", - EnvPassword: "secret", - EnvServerURL: "https://example.org:1234", - }, - expected: "gravity: some credentials information are missing: GRAVITY_USERNAME", - }, - { - desc: "missing EnvPassword", - envVars: map[string]string{ - EnvUsername: "user", - EnvPassword: "", - EnvServerURL: "https://example.org:1234", - }, - expected: "gravity: some credentials information are missing: GRAVITY_PASSWORD", - }, - { - desc: "missing EnvServerURL", - envVars: map[string]string{ - EnvUsername: "user", - EnvPassword: "secret", - EnvServerURL: "", - }, - expected: "gravity: some credentials information are missing: GRAVITY_SERVER_URL", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "gravity: some credentials information are missing: GRAVITY_USERNAME,GRAVITY_PASSWORD,GRAVITY_SERVER_URL", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - username string - password string - serverURL string - expected string - }{ - { - desc: "success", - username: "user", - password: "secret", - serverURL: "https://example.org:1234", - }, - { - desc: "missing username", - username: "", - password: "secret", - serverURL: "https://example.org:1234", - expected: "gravity: credentials missing", - }, - { - desc: "missing password", - username: "user", - password: "", - serverURL: "https://example.org:1234", - expected: "gravity: credentials missing", - }, - { - desc: "missing server URL", - username: "user", - password: "secret", - serverURL: "", - expected: "gravity: server URL missing", - }, - { - desc: "missing credentials", - expected: "gravity: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.Username = test.username - config.Password = test.password - config.ServerURL = test.serverURL - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - - config.Username = "user" - config.Password = "secret" - config.ServerURL = server.URL - - config.HTTPClient = server.Client() - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - return p, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("POST /api/v1/auth/login", - servermock.ResponseFromInternal("login.json"), - servermock.CheckRequestJSONBodyFromInternal("login-request.json")). - Route("GET /api/v1/dns/", - http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - if req.URL.Query().Get("name") != "example.com." { - servermock.ResponseFromInternal("zones.json").ServeHTTP(rw, req) - return - } - - servermock.ResponseFromInternal("zones_empty.json").ServeHTTP(rw, req) - }), - ). - Route("POST /api/v1/dns/zones/records", - servermock.Noop(). - WithStatusCode(http.StatusNoContent), - servermock.CheckQueryParameter().Strict(). - With("zone", "example.com."). - WithRegexp("uid", `\w{8}-\w{4}-\w{4}-\w{4}-\w{12}`). - With("hostname", "_acme-challenge")). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("DELETE /api/v1/dns/zones/records", - servermock.Noop(). - WithStatusCode(http.StatusNoContent), - servermock.CheckQueryParameter().Strict(). - With("zone", "example.com."). - With("uid", "123"). - With("type", "TXT"). - With("hostname", "_acme-challenge")). - Build(t) - - provider.records["abc"] = internal.Record{ - Fqdn: "example.com.", - Hostname: "_acme-challenge", - Type: "TXT", - UID: "123", - } - - err := provider.CleanUp("example.com", "abc", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/gravity/internal/client.go b/providers/dns/gravity/internal/client.go deleted file mode 100644 index 41c6294c3..000000000 --- a/providers/dns/gravity/internal/client.go +++ /dev/null @@ -1,234 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/http/cookiejar" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" - "golang.org/x/net/publicsuffix" -) - -// Client the Gravity API client. -type Client struct { - username string - password string - - baseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(serverURL, username, password string) (*Client, error) { - if username == "" || password == "" { - return nil, errors.New("credentials missing") - } - - if serverURL == "" { - return nil, errors.New("server URL missing") - } - - baseURL, err := url.Parse(serverURL) - if err != nil { - return nil, err - } - - return &Client{ - username: username, - password: password, - baseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -func (c *Client) Login(ctx context.Context) (*Auth, error) { - jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) - if err != nil { - return nil, err - } - - c.HTTPClient.Jar = jar - - login := Login{ - Username: c.username, - Password: c.password, - } - - endpoint := c.baseURL.JoinPath("api", "v1", "auth", "login") - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, login) - if err != nil { - return nil, err - } - - result := &Auth{} - - err = c.do(req, result) - if err != nil { - return nil, err - } - - return result, nil -} - -func (c *Client) Me(ctx context.Context) (*UserInfo, error) { - endpoint := c.baseURL.JoinPath("api", "v1", "auth", "me") - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - result := &UserInfo{} - - err = c.do(req, result) - if err != nil { - return nil, err - } - - return result, err -} - -func (c *Client) GetDNSZones(ctx context.Context, name string) ([]Zone, error) { - endpoint := c.baseURL.JoinPath("api", "v1", "dns", "zones") - - if name != "" { - query := endpoint.Query() - query.Set("name", name) - endpoint.RawQuery = query.Encode() - } - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - result := Zones{} - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return result.Zones, nil -} - -func (c *Client) CreateDNSRecord(ctx context.Context, zone string, record Record) error { - endpoint := c.baseURL.JoinPath("api", "v1", "dns", "zones", "records") - - query := endpoint.Query() - - query.Set("zone", zone) - query.Set("hostname", record.Hostname) - - // When the UID is the same as an existing one, the record is updated, else a new record is created. - // An explicit UID is not required to create a record. - if record.UID != "" { - query.Set("uid", record.UID) - } - - endpoint.RawQuery = query.Encode() - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) - if err != nil { - return err - } - - return c.do(req, nil) -} - -func (c *Client) DeleteDNSRecord(ctx context.Context, zone string, record Record) error { - endpoint := c.baseURL.JoinPath("api", "v1", "dns", "zones", "records") - - query := endpoint.Query() - - query.Set("zone", zone) - query.Set("hostname", record.Hostname) - query.Set("uid", record.UID) - query.Set("type", record.Type) - - endpoint.RawQuery = query.Encode() - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return err - } - - return c.do(req, nil) -} - -func (c *Client) do(req *http.Request, result any) error { - useragent.SetHeader(req.Header) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIError - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return &errAPI -} diff --git a/providers/dns/gravity/internal/client_test.go b/providers/dns/gravity/internal/client_test.go deleted file mode 100644 index 98b17c59e..000000000 --- a/providers/dns/gravity/internal/client_test.go +++ /dev/null @@ -1,160 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient(server.URL, "user", "secret") - if err != nil { - return nil, err - } - - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(), - ) -} - -func TestClient_Login(t *testing.T) { - client := mockBuilder(). - Route("POST /api/v1/auth/login", - http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - http.SetCookie(rw, &http.Cookie{ - Name: "gravity_session", - Value: "session_id", - Path: "/", - }) - - servermock.ResponseFromFixture("login.json").ServeHTTP(rw, req) - }), - servermock.CheckRequestJSONBodyFromFixture("login-request.json")). - Build(t) - - auth, err := client.Login(t.Context()) - require.NoError(t, err) - - cookies := client.HTTPClient.Jar.Cookies(client.baseURL) - - require.Len(t, cookies, 1) - - assert.Equal(t, "gravity_session", cookies[0].Name) - assert.Equal(t, "session_id", cookies[0].Value) - - expected := &Auth{Successful: true} - - assert.Equal(t, expected, auth) -} - -func TestClient_Login_error(t *testing.T) { - client := mockBuilder(). - Route("POST /api/v1/auth/login", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) - - _, err := client.Login(t.Context()) - require.EqualError(t, err, "status: UNAUTHENTICATED, error: unauthenticated, additionalProp1: string") -} - -func TestClient_Me(t *testing.T) { - client := mockBuilder(). - Route("GET /api/v1/auth/me", - servermock.ResponseFromFixture("me.json")). - Build(t) - - info, err := client.Me(t.Context()) - require.NoError(t, err) - - expected := &UserInfo{ - Username: "admin", - Authenticated: true, - Permissions: []Permission{{ - Methods: []string{"GET", "POST", "PUT", "HEAD", "DELETE"}, - Path: "/*", - }}, - } - - assert.Equal(t, expected, info) -} - -func TestClient_GetDNSZones(t *testing.T) { - client := mockBuilder(). - Route("GET /api/v1/dns/", - servermock.ResponseFromFixture("zones.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "example.com.")). - Build(t) - - zones, err := client.GetDNSZones(t.Context(), "example.com.") - require.NoError(t, err) - - expected := []Zone{{ - Name: "example.com.", - HandlerConfigs: []HandlerConfig{ - {Type: "memory"}, - {Type: "etcd"}, - }, - DefaultTTL: 86400, - RecordCount: 1, - }} - - assert.Equal(t, expected, zones) -} - -func TestClient_CreateDNSRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /api/v1/dns/zones/records", - servermock.Noop(). - WithStatusCode(http.StatusNoContent), - servermock.CheckRequestJSONBodyFromFixture("create_record-request.json"), - servermock.CheckQueryParameter().Strict(). - With("zone", "example.com."). - With("uid", "123"). - With("hostname", "_acme-challenge")). - Build(t) - - record := Record{ - Data: "txtTXTtxt", - Hostname: "_acme-challenge", - Type: "TXT", - UID: "123", - } - - err := client.CreateDNSRecord(t.Context(), "example.com.", record) - require.NoError(t, err) -} - -func TestClient_DeleteDNSRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /api/v1/dns/zones/records", - servermock.Noop(). - WithStatusCode(http.StatusNoContent), - servermock.CheckQueryParameter().Strict(). - With("zone", "example.com."). - With("uid", "123"). - With("type", "TXT"). - With("hostname", "_acme-challenge")). - Build(t) - - record := Record{ - Data: "txtTXTtxt", - Hostname: "_acme-challenge", - Type: "TXT", - UID: "123", - } - - err := client.DeleteDNSRecord(t.Context(), "example.com.", record) - require.NoError(t, err) -} diff --git a/providers/dns/gravity/internal/fixtures/create_record-request.json b/providers/dns/gravity/internal/fixtures/create_record-request.json deleted file mode 100644 index d671d1342..000000000 --- a/providers/dns/gravity/internal/fixtures/create_record-request.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "data": "txtTXTtxt", - "hostname": "_acme-challenge", - "type": "TXT", - "uid": "123" -} diff --git a/providers/dns/gravity/internal/fixtures/error.json b/providers/dns/gravity/internal/fixtures/error.json deleted file mode 100644 index 38b78fcca..000000000 --- a/providers/dns/gravity/internal/fixtures/error.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "code": 0, - "context": { - "additionalProp1": "string" - }, - "error": "unauthenticated", - "status": "UNAUTHENTICATED" -} diff --git a/providers/dns/gravity/internal/fixtures/login-request.json b/providers/dns/gravity/internal/fixtures/login-request.json deleted file mode 100644 index c641cd3e5..000000000 --- a/providers/dns/gravity/internal/fixtures/login-request.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "username": "user", - "password": "secret" -} diff --git a/providers/dns/gravity/internal/fixtures/login.json b/providers/dns/gravity/internal/fixtures/login.json deleted file mode 100644 index b9ae7145f..000000000 --- a/providers/dns/gravity/internal/fixtures/login.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "successful": true -} diff --git a/providers/dns/gravity/internal/fixtures/me.json b/providers/dns/gravity/internal/fixtures/me.json deleted file mode 100644 index 881a2ca5f..000000000 --- a/providers/dns/gravity/internal/fixtures/me.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "username": "admin", - "authenticated": true, - "permissions": [ - { - "path": "/*", - "methods": [ - "GET", - "POST", - "PUT", - "HEAD", - "DELETE" - ] - } - ] -} diff --git a/providers/dns/gravity/internal/fixtures/me_unauthenticated.json b/providers/dns/gravity/internal/fixtures/me_unauthenticated.json deleted file mode 100644 index 67698b8e2..000000000 --- a/providers/dns/gravity/internal/fixtures/me_unauthenticated.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "username": "", - "authenticated": false, - "permissions": null -} diff --git a/providers/dns/gravity/internal/fixtures/zones.json b/providers/dns/gravity/internal/fixtures/zones.json deleted file mode 100644 index 53a8df6c1..000000000 --- a/providers/dns/gravity/internal/fixtures/zones.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "zones": [ - { - "name": "example.com.", - "handlerConfigs": [ - { - "type": "memory" - }, - { - "type": "etcd" - } - ], - "defaultTTL": 86400, - "authoritative": false, - "hook": "", - "recordCount": 1 - } - ] -} diff --git a/providers/dns/gravity/internal/fixtures/zones_empty.json b/providers/dns/gravity/internal/fixtures/zones_empty.json deleted file mode 100644 index d8b70b45e..000000000 --- a/providers/dns/gravity/internal/fixtures/zones_empty.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "zones": null -} diff --git a/providers/dns/gravity/internal/types.go b/providers/dns/gravity/internal/types.go deleted file mode 100644 index 872bc070f..000000000 --- a/providers/dns/gravity/internal/types.go +++ /dev/null @@ -1,82 +0,0 @@ -package internal - -import ( - "fmt" - "strings" -) - -type APIError struct { - Status string `json:"status"` - ErrorMsg string `json:"error"` - Code int `json:"code"` - Context map[string]string `json:"context"` -} - -func (a *APIError) Error() string { - msg := new(strings.Builder) - - _, _ = fmt.Fprintf(msg, "status: %s, error: %s", a.Status, a.ErrorMsg) - - if a.Code != 0 { - _, _ = fmt.Fprintf(msg, ", code: %d", a.Code) - } - - if len(a.Context) != 0 { - for k, v := range a.Context { - _, _ = fmt.Fprintf(msg, ", %s: %s", k, v) - } - } - - return msg.String() -} - -type Login struct { - Username string `json:"username"` - Password string `json:"password"` -} - -type Auth struct { - Successful bool `json:"successful"` -} - -type UserInfo struct { - Username string `json:"username"` - Authenticated bool `json:"authenticated"` - Permissions []Permission `json:"permissions"` -} - -type Permission struct { - Methods []string `json:"methods"` - Path string `json:"path"` -} - -type Zones struct { - Zones []Zone `json:"zones"` -} - -type Zone struct { - Name string `json:"name"` - HandlerConfigs []HandlerConfig `json:"handlerConfigs"` - DefaultTTL int `json:"defaultTTL"` - Authoritative bool `json:"authoritative"` - Hook string `json:"hook"` - RecordCount int `json:"recordCount"` -} - -type HandlerConfig struct { - Type string `json:"type"` - CacheTTL int `json:"cache_ttl,omitempty"` - To []string `json:"to,omitempty"` -} - -type Record struct { - Data string `json:"data,omitempty"` - Fqdn string `json:"fqdn,omitempty"` - Hostname string `json:"hostname,omitempty"` - MxPreference int `json:"mxPreference,omitempty"` - SrvPort int `json:"srvPort,omitempty"` - SrvPriority int `json:"srvPriority,omitempty"` - SrvWeight int `json:"srvWeight,omitempty"` - Type string `json:"type,omitempty"` - UID string `json:"uid,omitempty"` -} diff --git a/providers/dns/hetzner/hetzner.go b/providers/dns/hetzner/hetzner.go index bae985b3e..5b2112d73 100644 --- a/providers/dns/hetzner/hetzner.go +++ b/providers/dns/hetzner/hetzner.go @@ -2,41 +2,34 @@ package hetzner import ( + "context" "errors" + "fmt" "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/hetzner/internal/hetznerv1" - "github.com/go-acme/lego/v4/providers/dns/hetzner/internal/legacy" -) - -// Environment variables names. -const ( - // Deprecated: use EnvAPIToken instead. - EnvAPIKey = legacy.EnvAPIKey - EnvAPIToken = hetznerv1.EnvAPIToken - - EnvTTL = hetznerv1.EnvTTL - EnvPropagationTimeout = hetznerv1.EnvPropagationTimeout - EnvPollingInterval = hetznerv1.EnvPollingInterval - EnvHTTPTimeout = hetznerv1.EnvHTTPTimeout + "github.com/go-acme/lego/v4/providers/dns/hetzner/internal" ) const minTTL = 60 -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) +// Environment variables names. +const ( + envNamespace = "HETZNER_" + + EnvAPIKey = envNamespace + "API_KEY" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) // Config is used to configure the creation of the DNSProvider. type Config struct { - // Deprecated: use APIToken instead - APIKey string - - APIToken string - + APIKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int @@ -48,7 +41,7 @@ func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, @@ -57,41 +50,22 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - provider challenge.ProviderTimeout + config *Config + client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for hetzner. +// Credentials must be passed in the environment variable: HETZNER_API_KEY. func NewDNSProvider() (*DNSProvider, error) { - foundAPIToken := env.GetOrFile(EnvAPIToken) != "" - foundAPIKey := env.GetOrFile(EnvAPIKey) != "" - - switch { - case foundAPIToken: - provider, err := hetznerv1.NewDNSProvider() - if err != nil { - return nil, err - } - - return &DNSProvider{provider: provider}, nil - - case foundAPIKey: - log.Warnf("APIKey (legacy Hetzner DNS API) is deprecated, please use APIToken (Hetzner Cloud API) instead.") - - provider, err := legacy.NewDNSProvider() - if err != nil { - return nil, err - } - - return &DNSProvider{provider: provider}, nil - - default: - provider, err := hetznerv1.NewDNSProvider() - if err != nil { - return nil, err - } - - return &DNSProvider{provider: provider}, nil + values, err := env.Get(EnvAPIKey) + if err != nil { + return nil, fmt.Errorf("hetzner: %w", err) } + + config := NewDefaultConfig() + config.APIKey = values[EnvAPIKey] + + return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for hetzner. @@ -100,57 +74,98 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("hetzner: the configuration of the DNS provider is nil") } - switch { - case config.APIToken != "": - cfg := &hetznerv1.Config{ - APIToken: config.APIToken, - PropagationTimeout: config.PropagationTimeout, - PollingInterval: config.PollingInterval, - TTL: config.TTL, - HTTPClient: config.HTTPClient, - } - - provider, err := hetznerv1.NewDNSProviderConfig(cfg) - if err != nil { - return nil, err - } - - return &DNSProvider{provider: provider}, nil - - case config.APIKey != "": - log.Warnf("%s (legacy Hetzner DNS API) is deprecated, please use %s (Hetzner Cloud API) instead.", EnvAPIKey, EnvAPIToken) - - cfg := &legacy.Config{ - APIKey: config.APIKey, - PropagationTimeout: config.PropagationTimeout, - PollingInterval: config.PollingInterval, - TTL: config.TTL, - HTTPClient: config.HTTPClient, - } - - provider, err := legacy.NewDNSProviderConfig(cfg) - if err != nil { - return nil, err - } - - return &DNSProvider{provider: provider}, nil + if config.APIKey == "" { + return nil, errors.New("hetzner: credentials missing") } - return nil, errors.New("hetzner: credentials missing") + if config.TTL < minTTL { + return nil, fmt.Errorf("hetzner: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) + } + + client := internal.NewClient(config.APIKey) + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + return &DNSProvider{config: config, client: client}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.provider.Timeout() + return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - return d.provider.Present(domain, token, keyAuth) + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("hetzner: could not find zone for domain %q: %w", domain, err) + } + + zone := dns01.UnFqdn(authZone) + + ctx := context.Background() + + zoneID, err := d.client.GetZoneID(ctx, zone) + if err != nil { + return fmt.Errorf("hetzner: %w", err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone) + if err != nil { + return fmt.Errorf("hetzner: %w", err) + } + + record := internal.DNSRecord{ + Type: "TXT", + Name: subDomain, + Value: info.Value, + TTL: d.config.TTL, + ZoneID: zoneID, + } + + if err := d.client.CreateRecord(ctx, record); err != nil { + return fmt.Errorf("hetzner: failed to add TXT record: fqdn=%s, zoneID=%s: %w", info.EffectiveFQDN, zoneID, err) + } + + return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - return d.provider.CleanUp(domain, token, keyAuth) + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("hetzner: could not find zone for domain %q: %w", domain, err) + } + + zone := dns01.UnFqdn(authZone) + + ctx := context.Background() + + zoneID, err := d.client.GetZoneID(ctx, zone) + if err != nil { + return fmt.Errorf("hetzner: %w", err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone) + if err != nil { + return fmt.Errorf("hetzner: %w", err) + } + + record, err := d.client.GetTxtRecord(ctx, subDomain, info.Value, zoneID) + if err != nil { + return fmt.Errorf("hetzner: %w", err) + } + + if err := d.client.DeleteRecord(ctx, record.ID); err != nil { + return fmt.Errorf("hetzner: failed to delete TXT record: id=%s, name=%s: %w", record.ID, record.Name, err) + } + + return nil } diff --git a/providers/dns/hetzner/hetzner.toml b/providers/dns/hetzner/hetzner.toml index 40d4cea72..19609f7db 100644 --- a/providers/dns/hetzner/hetzner.toml +++ b/providers/dns/hetzner/hetzner.toml @@ -5,18 +5,18 @@ Code = "hetzner" Since = "v3.7.0" Example = ''' -HETZNER_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns hetzner -d '*.example.com' -d example.com run +HETZNER_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ +lego --email you@example.com --dns hetzner --domains my.example.org run ''' [Configuration] [Configuration.Credentials] - HETZNER_API_TOKEN = "API token" + HETZNER_API_KEY = "API key" [Configuration.Additional] - HETZNER_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - HETZNER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - HETZNER_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - HETZNER_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + HETZNER_POLLING_INTERVAL = "Time between DNS propagation check" + HETZNER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + HETZNER_TTL = "The TTL of the TXT record used for the DNS challenge" + HETZNER_HTTP_TIMEOUT = "API request timeout" [Links] - API = "https://docs.hetzner.cloud/reference/cloud#dns" + API = "https://dns.hetzner.com/api-docs" diff --git a/providers/dns/hetzner/hetzner_test.go b/providers/dns/hetzner/hetzner_test.go index 430f0270b..d028fd06b 100644 --- a/providers/dns/hetzner/hetzner_test.go +++ b/providers/dns/hetzner/hetzner_test.go @@ -3,72 +3,52 @@ package hetzner import ( "testing" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/providers/dns/hetzner/internal/hetznerv1" - "github.com/go-acme/lego/v4/providers/dns/hetzner/internal/legacy" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -var envTest = tester.NewEnvTest(EnvAPIKey, EnvAPIToken) +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest( + EnvAPIKey). + WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { - desc string - envVars map[string]string - - expectedProvider challenge.ProviderTimeout - expectedError string + desc string + envVars map[string]string + expected string }{ { - desc: "success (v1)", - envVars: map[string]string{ - EnvAPIToken: "123", - }, - expectedProvider: &hetznerv1.DNSProvider{}, - }, - { - desc: "success (legacy)", + desc: "success", envVars: map[string]string{ EnvAPIKey: "123", }, - expectedProvider: &legacy.DNSProvider{}, - }, - { - desc: "success (both)", - envVars: map[string]string{ - EnvAPIKey: "123", - EnvAPIToken: "123", - }, - expectedProvider: &hetznerv1.DNSProvider{}, }, { desc: "missing credentials", envVars: map[string]string{ - EnvAPIKey: "", - EnvAPIToken: "", + EnvAPIKey: "", }, - expectedError: "hetzner: some credentials information are missing: HETZNER_API_TOKEN", + expected: "hetzner: some credentials information are missing: HETZNER_API_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() - if test.expectedError == "" { + if test.expected == "" { require.NoError(t, err) - assert.IsType(t, test.expectedProvider, p.provider) require.NotNil(t, p) + require.NotNil(t, p.config) } else { - require.EqualError(t, err, test.expectedError) + require.EqualError(t, err, test.expected) } }) } @@ -78,53 +58,68 @@ func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiKey string - apiToken string ttl int - - expectedProvider challenge.ProviderTimeout - expectedError string + expected string }{ { - desc: "success (v1)", - ttl: minTTL, - apiToken: "123", - expectedProvider: &hetznerv1.DNSProvider{}, + desc: "success", + ttl: minTTL, + apiKey: "123", }, { - desc: "success (legacy)", - ttl: minTTL, - apiKey: "456", - expectedProvider: &legacy.DNSProvider{}, + desc: "missing credentials", + ttl: minTTL, + expected: "hetzner: credentials missing", }, { - desc: "success (both)", - ttl: minTTL, - apiToken: "123", - apiKey: "456", - expectedProvider: &hetznerv1.DNSProvider{}, - }, - { - desc: "missing credentials", - ttl: minTTL, - expectedError: "hetzner: credentials missing", + desc: "invalid TTL", + apiKey: "123", + ttl: 10, + expected: "hetzner: invalid TTL, TTL (10) must be greater than 60", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() - config.APIToken = test.apiToken config.APIKey = test.apiKey config.TTL = test.ttl p, err := NewDNSProviderConfig(config) - if test.expectedError == "" { + if test.expected == "" { require.NoError(t, err) - assert.IsType(t, test.expectedProvider, p.provider) + require.NotNil(t, p) + require.NotNil(t, p.config) } else { - require.EqualError(t, err, test.expectedError) + require.EqualError(t, err, test.expected) } }) } } + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/hetzner/internal/legacy/internal/client.go b/providers/dns/hetzner/internal/client.go similarity index 99% rename from providers/dns/hetzner/internal/legacy/internal/client.go rename to providers/dns/hetzner/internal/client.go index cd187f6e5..381922264 100644 --- a/providers/dns/hetzner/internal/legacy/internal/client.go +++ b/providers/dns/hetzner/internal/client.go @@ -83,7 +83,6 @@ func (c *Client) getRecords(ctx context.Context, zoneID string) (*DNSRecords, er } records := &DNSRecords{} - err = json.Unmarshal(raw, records) if err != nil { return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) @@ -191,7 +190,6 @@ func (c *Client) getZones(ctx context.Context, name string) (*Zones, error) { } zones := &Zones{} - err = json.Unmarshal(raw, zones) if err != nil { return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) diff --git a/providers/dns/hetzner/internal/client_test.go b/providers/dns/hetzner/internal/client_test.go new file mode 100644 index 000000000..aa2175409 --- /dev/null +++ b/providers/dns/hetzner/internal/client_test.go @@ -0,0 +1,176 @@ +package internal + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupTest(t *testing.T, apiKey string) (*Client, *http.ServeMux) { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient(apiKey) + client.baseURL, _ = url.Parse(server.URL) + client.HTTPClient = server.Client() + + return client, mux +} + +func TestClient_GetTxtRecord(t *testing.T) { + const zoneID = "zoneA" + const apiKey = "myKeyA" + + client, mux := setupTest(t, apiKey) + + mux.HandleFunc("/api/v1/records", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + auth := req.Header.Get(authHeader) + if auth != apiKey { + http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized) + return + } + + zID := req.URL.Query().Get("zone_id") + if zID != zoneID { + http.Error(rw, fmt.Sprintf("invalid zone ID: %s", zID), http.StatusBadRequest) + return + } + + file, err := os.Open("./fixtures/get_txt_record.json") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + record, err := client.GetTxtRecord(context.Background(), "test1", "txttxttxt", zoneID) + require.NoError(t, err) + + fmt.Println(record) +} + +func TestClient_CreateRecord(t *testing.T) { + const zoneID = "zoneA" + const apiKey = "myKeyB" + + client, mux := setupTest(t, apiKey) + + mux.HandleFunc("/api/v1/records", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + auth := req.Header.Get(authHeader) + if auth != apiKey { + http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized) + return + } + + file, err := os.Open("./fixtures/create_txt_record.json") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + record := DNSRecord{ + Name: "test", + Type: "TXT", + Value: "txttxttxt", + TTL: 600, + ZoneID: zoneID, + } + + err := client.CreateRecord(context.Background(), record) + require.NoError(t, err) +} + +func TestClient_DeleteRecord(t *testing.T) { + const apiKey = "myKeyC" + + client, mux := setupTest(t, apiKey) + + mux.HandleFunc("/api/v1/records/recordID", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodDelete { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + auth := req.Header.Get(authHeader) + if auth != apiKey { + http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized) + return + } + }) + + err := client.DeleteRecord(context.Background(), "recordID") + require.NoError(t, err) +} + +func TestClient_GetZoneID(t *testing.T) { + const apiKey = "myKeyD" + + client, mux := setupTest(t, apiKey) + + mux.HandleFunc("/api/v1/zones", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + auth := req.Header.Get(authHeader) + if auth != apiKey { + http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized) + return + } + + file, err := os.Open("./fixtures/get_zone_id.json") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + zoneID, err := client.GetZoneID(context.Background(), "example.com") + require.NoError(t, err) + + assert.Equal(t, "zoneA", zoneID) +} diff --git a/providers/dns/hetzner/internal/legacy/internal/fixtures/create_txt_record.json b/providers/dns/hetzner/internal/fixtures/create_txt_record.json similarity index 100% rename from providers/dns/hetzner/internal/legacy/internal/fixtures/create_txt_record.json rename to providers/dns/hetzner/internal/fixtures/create_txt_record.json diff --git a/providers/dns/hetzner/internal/legacy/internal/fixtures/get_txt_record.json b/providers/dns/hetzner/internal/fixtures/get_txt_record.json similarity index 100% rename from providers/dns/hetzner/internal/legacy/internal/fixtures/get_txt_record.json rename to providers/dns/hetzner/internal/fixtures/get_txt_record.json diff --git a/providers/dns/hetzner/internal/legacy/internal/fixtures/get_zone_id.json b/providers/dns/hetzner/internal/fixtures/get_zone_id.json similarity index 100% rename from providers/dns/hetzner/internal/legacy/internal/fixtures/get_zone_id.json rename to providers/dns/hetzner/internal/fixtures/get_zone_id.json diff --git a/providers/dns/hetzner/internal/hetznerv1/fixtures/add_rrset_records-request.json b/providers/dns/hetzner/internal/hetznerv1/fixtures/add_rrset_records-request.json deleted file mode 100644 index 210f84435..000000000 --- a/providers/dns/hetzner/internal/hetznerv1/fixtures/add_rrset_records-request.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "ttl": 120, - "records": [ - { - "value": "\"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\"" - } - ] -} diff --git a/providers/dns/hetzner/internal/hetznerv1/fixtures/add_rrset_records.json b/providers/dns/hetzner/internal/hetznerv1/fixtures/add_rrset_records.json deleted file mode 100644 index 2341c7e6e..000000000 --- a/providers/dns/hetzner/internal/hetznerv1/fixtures/add_rrset_records.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "action": { - "id": 1, - "command": "add_rrset_records", - "status": "running", - "progress": 50, - "started": "2016-01-30T23:55:00+00:00", - "finished": null, - "resources": [ - { - "id": 42, - "type": "zone" - } - ], - "error": null - } -} diff --git a/providers/dns/hetzner/internal/hetznerv1/fixtures/get_action_error.json b/providers/dns/hetzner/internal/hetznerv1/fixtures/get_action_error.json deleted file mode 100644 index 2a4472f67..000000000 --- a/providers/dns/hetzner/internal/hetznerv1/fixtures/get_action_error.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "action": { - "id": 1, - "command": "remove_rrset_records", - "status": "error", - "started": "2016-01-30T23:55:00+00:00", - "finished": "2016-01-30T23:55:00+00:00", - "progress": 50, - "resources": [ - { - "id": 42, - "type": "zone" - } - ], - "error": { - "code": "action_failed", - "message": "Action failed" - } - } -} diff --git a/providers/dns/hetzner/internal/hetznerv1/fixtures/get_action_running.json b/providers/dns/hetzner/internal/hetznerv1/fixtures/get_action_running.json deleted file mode 100644 index dcec6c2cd..000000000 --- a/providers/dns/hetzner/internal/hetznerv1/fixtures/get_action_running.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "action": { - "id": 1, - "command": "remove_rrset_records", - "status": "running", - "started": "2016-01-30T23:55:00+00:00", - "finished": "2016-01-30T23:55:00+00:00", - "progress": 50, - "resources": [ - { - "id": 42, - "type": "zone" - } - ] - } -} diff --git a/providers/dns/hetzner/internal/hetznerv1/fixtures/get_action_success.json b/providers/dns/hetzner/internal/hetznerv1/fixtures/get_action_success.json deleted file mode 100644 index 6b7267c07..000000000 --- a/providers/dns/hetzner/internal/hetznerv1/fixtures/get_action_success.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "action": { - "id": 1, - "command": "remove_rrset_records", - "status": "success", - "started": "2016-01-30T23:55:00+00:00", - "finished": "2016-01-30T23:55:00+00:00", - "progress": 100, - "resources": [ - { - "id": 42, - "type": "zone" - } - ] - } -} diff --git a/providers/dns/hetzner/internal/hetznerv1/fixtures/remove_rrset_records-request.json b/providers/dns/hetzner/internal/hetznerv1/fixtures/remove_rrset_records-request.json deleted file mode 100644 index 982273b67..000000000 --- a/providers/dns/hetzner/internal/hetznerv1/fixtures/remove_rrset_records-request.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "records": [ - { - "value": "\"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\"" - } - ] -} diff --git a/providers/dns/hetzner/internal/hetznerv1/fixtures/remove_rrset_records.json b/providers/dns/hetzner/internal/hetznerv1/fixtures/remove_rrset_records.json deleted file mode 100644 index 1b10dfd5e..000000000 --- a/providers/dns/hetzner/internal/hetznerv1/fixtures/remove_rrset_records.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "action": { - "id": 1, - "command": "remove_rrset_records", - "status": "running", - "progress": 50, - "started": "2016-01-30T23:55:00+00:00", - "finished": null, - "resources": [ - { - "id": 42, - "type": "zone" - } - ], - "error": null - } -} diff --git a/providers/dns/hetzner/internal/hetznerv1/hetznerv1.go b/providers/dns/hetzner/internal/hetznerv1/hetznerv1.go deleted file mode 100644 index b31c766ce..000000000 --- a/providers/dns/hetzner/internal/hetznerv1/hetznerv1.go +++ /dev/null @@ -1,209 +0,0 @@ -// Package hetznerv1 implements a DNS provider for solving the DNS-01 challenge using Hetzner. -package hetznerv1 - -import ( - "context" - "errors" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/cenkalti/backoff/v5" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/platform/wait" - "github.com/go-acme/lego/v4/providers/dns/hetzner/internal/hetznerv1/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "golang.org/x/net/idna" -) - -// Environment variables names. -const ( - envNamespace = "HETZNER_" - - EnvAPIToken = envNamespace + "API_TOKEN" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIToken string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for Hetzner. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIToken) - if err != nil { - return nil, fmt.Errorf("hetzner: %w", err) - } - - config := NewDefaultConfig() - config.APIToken = values[EnvAPIToken] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Hetzner. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("hetzner: the configuration of the DNS provider is nil") - } - - if config.APIToken == "" { - return nil, errors.New("hetzner: credentials missing") - } - - client, err := internal.NewClient( - clientdebug.Wrap( - internal.OAuthStaticAccessToken(config.HTTPClient, config.APIToken), - ), - ) - if err != nil { - return nil, fmt.Errorf("hetzner: %w", err) - } - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("hetzner: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("hetzner: %w", err) - } - - subDomainPunnycoded, err := idna.ToASCII(dns01.UnFqdn(subDomain)) - if err != nil { - return fmt.Errorf("hetzner: %w", err) - } - - zone, err := idna.ToASCII(dns01.UnFqdn(authZone)) - if err != nil { - return fmt.Errorf("hetzner: %w", err) - } - - records := []internal.Record{{Value: strconv.Quote(info.Value)}} - - action, err := d.client.AddRRSetRecords(ctx, zone, "TXT", subDomainPunnycoded, d.config.TTL, records) - if err != nil { - return fmt.Errorf("hetzner: add RRSet records: %w", err) - } - - err = d.waitAction(ctx, action.ID) - if err != nil { - return fmt.Errorf("hetzner: wait (add RRSet records): %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("hetzner: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("hetzner: %w", err) - } - - subDomainPunnycoded, err := idna.ToASCII(dns01.UnFqdn(subDomain)) - if err != nil { - return fmt.Errorf("hetzner: %w", err) - } - - zone, err := idna.ToASCII(dns01.UnFqdn(authZone)) - if err != nil { - return fmt.Errorf("hetzner: %w", err) - } - - records := []internal.Record{{Value: strconv.Quote(info.Value)}} - - action, err := d.client.RemoveRRSetRecords(ctx, zone, "TXT", subDomainPunnycoded, records) - if err != nil { - return fmt.Errorf("hetzner: remove RRSet records: %w", err) - } - - err = d.waitAction(ctx, action.ID) - if err != nil { - return fmt.Errorf("hetzner: wait (remove RRSet records): %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -func (d *DNSProvider) waitAction(ctx context.Context, actionID int64) error { - return wait.Retry(ctx, - func() error { - result, err := d.client.GetAction(ctx, actionID) - if err != nil { - return backoff.Permanent(fmt.Errorf("get action %d: %w", actionID, err)) - } - - switch result.Status { - case internal.StatusRunning: - return fmt.Errorf("action %d is %s", actionID, internal.StatusRunning) - - case internal.StatusError: - return backoff.Permanent(fmt.Errorf("action %d: %s: %w", actionID, internal.StatusError, result.ErrorInfo)) - - default: - return nil - } - }, - backoff.WithBackOff(backoff.NewConstantBackOff(d.config.PollingInterval)), - backoff.WithMaxElapsedTime(d.config.PropagationTimeout), - ) -} diff --git a/providers/dns/hetzner/internal/hetznerv1/hetznerv1_test.go b/providers/dns/hetzner/internal/hetznerv1/hetznerv1_test.go deleted file mode 100644 index bf52baa35..000000000 --- a/providers/dns/hetzner/internal/hetznerv1/hetznerv1_test.go +++ /dev/null @@ -1,232 +0,0 @@ -package hetznerv1 - -import ( - "net/http/httptest" - "net/url" - "testing" - "time" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAPIToken).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAPIToken: "secret", - }, - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "hetzner: some credentials information are missing: HETZNER_API_TOKEN", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiToken string - expected string - }{ - { - desc: "success", - apiToken: "secret", - }, - { - desc: "missing credentials", - expected: "hetzner: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIToken = test.apiToken - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIToken = "secret" - config.HTTPClient = server.Client() - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - p.client.BaseURL, _ = url.Parse(server.URL) - - return p, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(). - WithAuthorization("Bearer secret"), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("POST /zones/example.com/rrsets/_acme-challenge/TXT/actions/add_records", - servermock.ResponseFromFixture("add_rrset_records.json"), - servermock.CheckRequestJSONBodyFromFixture("add_rrset_records-request.json")). - Route("GET /actions/1", - servermock.ResponseFromFixture("get_action_success.json")). - Build(t) - - err := provider.Present("example.com", "", "foobar") - require.NoError(t, err) -} - -func TestDNSProvider_Present_error(t *testing.T) { - provider := mockBuilder(). - Route("POST /zones/example.com/rrsets/_acme-challenge/TXT/actions/add_records", - servermock.ResponseFromFixture("add_rrset_records.json"), - servermock.CheckRequestJSONBodyFromFixture("add_rrset_records-request.json")). - Route("GET /actions/1", - servermock.ResponseFromFixture("get_action_error.json")). - Build(t) - - provider.config.PollingInterval = 20 * time.Millisecond - provider.config.PropagationTimeout = 1 * time.Second - - err := provider.Present("example.com", "", "foobar") - require.EqualError(t, err, "hetzner: wait (add RRSet records): action 1: error: action_failed: Action failed") -} - -func TestDNSProvider_Present_running(t *testing.T) { - provider := mockBuilder(). - Route("POST /zones/example.com/rrsets/_acme-challenge/TXT/actions/add_records", - servermock.ResponseFromFixture("add_rrset_records.json"), - servermock.CheckRequestJSONBodyFromFixture("add_rrset_records-request.json")). - Route("GET /actions/1", - servermock.ResponseFromFixture("get_action_running.json")). - Build(t) - - provider.config.PollingInterval = 20 * time.Millisecond - provider.config.PropagationTimeout = 1 * time.Second - - err := provider.Present("example.com", "", "foobar") - require.EqualError(t, err, "hetzner: wait (add RRSet records): action 1 is running") -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("POST /zones/example.com/rrsets/_acme-challenge/TXT/actions/remove_records", - servermock.ResponseFromFixture("remove_rrset_records.json"), - servermock.CheckRequestJSONBodyFromFixture("remove_rrset_records-request.json")). - Route("GET /actions/1", - servermock.ResponseFromFixture("get_action_success.json")). - Build(t) - - err := provider.CleanUp("example.com", "", "foobar") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp_error(t *testing.T) { - provider := mockBuilder(). - Route("POST /zones/example.com/rrsets/_acme-challenge/TXT/actions/remove_records", - servermock.ResponseFromFixture("remove_rrset_records.json"), - servermock.CheckRequestJSONBodyFromFixture("remove_rrset_records-request.json")). - Route("GET /actions/1", - servermock.ResponseFromFixture("get_action_error.json")). - Build(t) - - provider.config.PollingInterval = 20 * time.Millisecond - provider.config.PropagationTimeout = 1 * time.Second - - err := provider.CleanUp("example.com", "", "foobar") - require.EqualError(t, err, "hetzner: wait (remove RRSet records): action 1: error: action_failed: Action failed") -} - -func TestDNSProvider_CleanUp_running(t *testing.T) { - provider := mockBuilder(). - Route("POST /zones/example.com/rrsets/_acme-challenge/TXT/actions/remove_records", - servermock.ResponseFromFixture("remove_rrset_records.json"), - servermock.CheckRequestJSONBodyFromFixture("remove_rrset_records-request.json")). - Route("GET /actions/1", - servermock.ResponseFromFixture("get_action_running.json")). - Build(t) - - provider.config.PollingInterval = 20 * time.Millisecond - provider.config.PropagationTimeout = 1 * time.Second - - err := provider.CleanUp("example.com", "", "foobar") - require.EqualError(t, err, "hetzner: wait (remove RRSet records): action 1 is running") -} diff --git a/providers/dns/hetzner/internal/hetznerv1/internal/client.go b/providers/dns/hetzner/internal/hetznerv1/internal/client.go deleted file mode 100644 index 2f29f642a..000000000 --- a/providers/dns/hetzner/internal/hetznerv1/internal/client.go +++ /dev/null @@ -1,183 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "golang.org/x/oauth2" -) - -const defaultBaseURL = "https://api.hetzner.cloud/v1" - -const ( - StatusRunning = "running" - StatusSuccess = "success" - StatusError = "error" -) - -// Client the Hetzner API client. -type Client struct { - BaseURL *url.URL - httpClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(hc *http.Client) (*Client, error) { - baseURL, _ := url.Parse(defaultBaseURL) - - if hc == nil { - hc = &http.Client{Timeout: 10 * time.Second} - } - - return &Client{ - BaseURL: baseURL, - httpClient: hc, - }, nil -} - -// AddRRSetRecords adds records to an RRSet. -// https://docs.hetzner.cloud/reference/cloud#zone-rrset-actions-add-records-to-an-rrset -func (c *Client) AddRRSetRecords(ctx context.Context, zoneIDName, recordType, recordName string, ttl int, records []Record) (*Action, error) { - endpoint := c.BaseURL.JoinPath("zones", zoneIDName, "rrsets", recordName, recordType, "actions", "add_records") - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, RRSet{TTL: ttl, Records: records}) - if err != nil { - return nil, err - } - - var result ActionResponse - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return result.Action, nil -} - -// RemoveRRSetRecords removes records from an RRSet. -// https://docs.hetzner.cloud/reference/cloud#zone-rrset-actions-remove-records-from-an-rrset -func (c *Client) RemoveRRSetRecords(ctx context.Context, zoneIDName, recordType, recordName string, records []Record) (*Action, error) { - endpoint := c.BaseURL.JoinPath("zones", zoneIDName, "rrsets", recordName, recordType, "actions", "remove_records") - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, RRSet{Records: records}) - if err != nil { - return nil, err - } - - var result ActionResponse - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return result.Action, nil -} - -// GetAction gets an action. -// https://docs.hetzner.cloud/reference/cloud#actions-get-an-action -func (c *Client) GetAction(ctx context.Context, id int64) (*Action, error) { - endpoint := c.BaseURL.JoinPath("actions", strconv.FormatInt(id, 10)) - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var result ActionResponse - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return result.Action, nil -} - -func (c *Client) do(req *http.Request, result any) error { - resp, err := c.httpClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIError - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return &errAPI -} - -func OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client { - if client == nil { - client = &http.Client{Timeout: 5 * time.Second} - } - - client.Transport = &oauth2.Transport{ - Source: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}), - Base: client.Transport, - } - - return client -} diff --git a/providers/dns/hetzner/internal/hetznerv1/internal/client_test.go b/providers/dns/hetzner/internal/hetznerv1/internal/client_test.go deleted file mode 100644 index 6fd3d77a7..000000000 --- a/providers/dns/hetzner/internal/hetznerv1/internal/client_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient(OAuthStaticAccessToken(server.Client(), "secret")) - if err != nil { - return nil, err - } - - client.BaseURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(). - WithAuthorization("Bearer secret"), - ) -} - -func TestClient_AddRRSetRecords(t *testing.T) { - client := mockBuilder(). - Route("POST /zones/example.com/rrsets/www/TXT/actions/add_records", - servermock.ResponseFromFixture("add_rrset_records.json"), - servermock.CheckRequestJSONBodyFromFixture("add_rrset_records-request.json")). - Build(t) - - records := []Record{{ - Value: "198.51.100.1", - Comment: "My web server at Hetzner Cloud.", - }} - - result, err := client.AddRRSetRecords(t.Context(), "example.com", "TXT", "www", 3600, records) - require.NoError(t, err) - - expected := &Action{ - ID: 1, - Command: "add_rrset_records", - Status: "running", - Progress: 50, - Resources: []Resources{{ID: 590000000000000, Type: "zone"}}, - } - - assert.Equal(t, expected, result) -} - -func TestClient_AddRRSetRecords_error_invalid_input(t *testing.T) { - client := mockBuilder(). - Route("POST /zones/example.com/rrsets/www/TXT/actions/add_records", - servermock.ResponseFromFixture("error-invalid_input.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) - - records := []Record{{ - Value: "198.51.100.1", - Comment: "My web server at Hetzner Cloud.", - }} - - _, err := client.AddRRSetRecords(t.Context(), "example.com", "TXT", "www", 0, records) - require.EqualError(t, err, "invalid_input: invalid input in field 'broken_field': is too longfield: broken_field: is too long") -} - -func TestClient_AddRRSetRecords_error_resource_limit_exceeded(t *testing.T) { - client := mockBuilder(). - Route("POST /zones/example.com/rrsets/www/TXT/actions/add_records", - servermock.ResponseFromFixture("error-resource_limit_exceeded.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) - - records := []Record{{ - Value: "198.51.100.1", - Comment: "My web server at Hetzner Cloud.", - }} - - _, err := client.AddRRSetRecords(t.Context(), "example.com", "TXT", "www", 0, records) - require.EqualError(t, err, "resource_limit_exceeded: project limit exceededlimit: project_limit") -} - -func TestClient_AddRRSetRecords_error_deprecated_api_endpoint(t *testing.T) { - client := mockBuilder(). - Route("POST /zones/example.com/rrsets/www/TXT/actions/add_records", - servermock.ResponseFromFixture("error-deprecated_api_endpoint.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) - - records := []Record{{ - Value: "198.51.100.1", - Comment: "My web server at Hetzner Cloud.", - }} - - _, err := client.AddRRSetRecords(t.Context(), "example.com", "TXT", "www", 0, records) - require.EqualError(t, err, "deprecated_api_endpoint: API functionality was removed: https://docs.hetzner.cloud/changelog#2023-07-20-foo-endpoint-is-deprecated") -} - -func TestClient_RemoveRRSetRecords(t *testing.T) { - client := mockBuilder(). - Route("POST /zones/example.com/rrsets/www/TXT/actions/remove_records", - servermock.ResponseFromFixture("remove_rrset_records.json"), - servermock.CheckRequestJSONBodyFromFixture("remove_rrset_records-request.json")). - Build(t) - - records := []Record{{ - Value: "198.51.100.1", - Comment: "My web server at Hetzner Cloud.", - }} - - result, err := client.RemoveRRSetRecords(t.Context(), "example.com", "TXT", "www", records) - require.NoError(t, err) - - expected := &Action{ - ID: 1, - Command: "remove_rrset_records", - Status: "running", - Progress: 50, - Resources: []Resources{{ID: 42, Type: "zone"}}, - } - - assert.Equal(t, expected, result) -} - -func TestClient_GetAction(t *testing.T) { - client := mockBuilder(). - Route("GET /actions/123", servermock.ResponseFromFixture("get_action.json")). - Route("/", servermock.DumpRequest()). - Build(t) - - result, err := client.GetAction(t.Context(), 123) - require.NoError(t, err) - - expected := &Action{ - ID: 590000000000000, - Command: "start_resource", - Status: "running", - Progress: 100, - Resources: []Resources{{ID: 590000000000000, Type: "server"}}, - ErrorInfo: &ErrorInfo{ - Code: "action_failed", - Message: "Action failed", - }, - } - - assert.Equal(t, expected, result) -} diff --git a/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/add_rrset_records-request.json b/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/add_rrset_records-request.json deleted file mode 100644 index cba0f34d3..000000000 --- a/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/add_rrset_records-request.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ttl": 3600, - "records": [ - { - "value": "198.51.100.1", - "comment": "My web server at Hetzner Cloud." - } - ] -} diff --git a/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/add_rrset_records.json b/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/add_rrset_records.json deleted file mode 100644 index 7267b02cb..000000000 --- a/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/add_rrset_records.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "action": { - "id": 1, - "command": "add_rrset_records", - "status": "running", - "progress": 50, - "started": "2016-01-30T23:55:00+00:00", - "finished": null, - "resources": [ - { - "id": 590000000000000, - "type": "zone" - } - ], - "error": null - } -} diff --git a/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/error-deprecated_api_endpoint.json b/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/error-deprecated_api_endpoint.json deleted file mode 100644 index 4d8fb945d..000000000 --- a/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/error-deprecated_api_endpoint.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "error": { - "code": "deprecated_api_endpoint", - "message": "API functionality was removed", - "details": { - "announcement": "https://docs.hetzner.cloud/changelog#2023-07-20-foo-endpoint-is-deprecated" - } - } -} diff --git a/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/error-invalid_input.json b/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/error-invalid_input.json deleted file mode 100644 index e05bf7a3e..000000000 --- a/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/error-invalid_input.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "error": { - "code": "invalid_input", - "message": "invalid input in field 'broken_field': is too long", - "details": { - "fields": [ - { - "name": "broken_field", - "messages": [ - "is too long" - ] - } - ] - } - } -} diff --git a/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/error-resource_limit_exceeded.json b/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/error-resource_limit_exceeded.json deleted file mode 100644 index 9072d10e3..000000000 --- a/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/error-resource_limit_exceeded.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "error": { - "code": "resource_limit_exceeded", - "message": "project limit exceeded", - "details": { - "limits": [ - { - "name": "project_limit" - } - ] - } - } -} diff --git a/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/get_action.json b/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/get_action.json deleted file mode 100644 index 19278fc51..000000000 --- a/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/get_action.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "action": { - "id": 590000000000000, - "command": "start_resource", - "status": "running", - "started": "2016-01-30T23:55:00+00:00", - "finished": "2016-01-30T23:55:00+00:00", - "progress": 100, - "resources": [ - { - "id": 590000000000000, - "type": "server" - } - ], - "error": { - "code": "action_failed", - "message": "Action failed" - } - } -} diff --git a/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/remove_rrset_records-request.json b/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/remove_rrset_records-request.json deleted file mode 100644 index 778e051b4..000000000 --- a/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/remove_rrset_records-request.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "records": [ - { - "value": "198.51.100.1", - "comment": "My web server at Hetzner Cloud." - } - ] -} diff --git a/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/remove_rrset_records.json b/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/remove_rrset_records.json deleted file mode 100644 index 1b10dfd5e..000000000 --- a/providers/dns/hetzner/internal/hetznerv1/internal/fixtures/remove_rrset_records.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "action": { - "id": 1, - "command": "remove_rrset_records", - "status": "running", - "progress": 50, - "started": "2016-01-30T23:55:00+00:00", - "finished": null, - "resources": [ - { - "id": 42, - "type": "zone" - } - ], - "error": null - } -} diff --git a/providers/dns/hetzner/internal/hetznerv1/internal/types.go b/providers/dns/hetzner/internal/hetznerv1/internal/types.go deleted file mode 100644 index 2b38a8a8c..000000000 --- a/providers/dns/hetzner/internal/hetznerv1/internal/types.go +++ /dev/null @@ -1,98 +0,0 @@ -package internal - -import ( - "fmt" - "strings" -) - -type APIError struct { - ErrorInfo ErrorInfo `json:"error"` -} - -type ErrorInfo struct { - Code string `json:"code,omitempty"` - Message string `json:"message,omitempty"` - Details ErrorDetails `json:"details"` -} - -func (i *ErrorInfo) Error() string { - msg := new(strings.Builder) - - _, _ = fmt.Fprintf(msg, "%s: %s", i.Code, i.Message) - - if i.Details.Announcement != "" { - _, _ = fmt.Fprintf(msg, ": %s", i.Details.Announcement) - } - - for _, limit := range i.Details.Limits { - _, _ = fmt.Fprintf(msg, "limit: %s", limit.Name) - } - - for _, field := range i.Details.Fields { - _, _ = fmt.Fprintf(msg, "field: %s: %s", field.Name, strings.Join(field.Messages, ", ")) - } - - return msg.String() -} - -type ErrorDetails struct { - Announcement string `json:"announcement,omitempty"` - Limits []LimitError `json:"limits,omitempty"` - Fields []FieldError `json:"fields,omitempty"` -} - -type FieldError struct { - Name string `json:"name,omitempty"` - Messages []string `json:"messages,omitempty"` -} - -type LimitError struct { - Name string `json:"name,omitempty"` -} - -func (a *APIError) Error() string { - return a.ErrorInfo.Error() -} - -type RRSet struct { - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` - TTL int `json:"ttl,omitempty"` - Labels map[string]string `json:"labels,omitempty"` - Protection *Protection `json:"protection,omitempty"` - Records []Record `json:"records,omitempty"` - ZoneID int `json:"zone,omitempty"` -} - -type Protection struct { - Change bool `json:"change,omitempty"` -} - -type Record struct { - Value string `json:"value,omitempty"` - Comment string `json:"comment,omitempty"` -} - -type ActionResponse struct { - Action *Action `json:"action,omitempty"` -} - -type Action struct { - ID int64 `json:"id,omitempty"` - Command string `json:"command,omitempty"` - - // It can be: `running`, `success`, `error`. - // https://docs.hetzner.cloud/reference/cloud#zone-actions-get-an-action - // https://docs.hetzner.cloud/reference/cloud#zone-actions - Status string `json:"status,omitempty"` - Progress int `json:"progress,omitempty"` - - Resources []Resources `json:"resources,omitempty"` - ErrorInfo *ErrorInfo `json:"error,omitempty"` -} - -type Resources struct { - ID int64 `json:"id,omitempty"` - Type string `json:"type,omitempty"` -} diff --git a/providers/dns/hetzner/internal/legacy/hetzner.go b/providers/dns/hetzner/internal/legacy/hetzner.go deleted file mode 100644 index 393a3d671..000000000 --- a/providers/dns/hetzner/internal/legacy/hetzner.go +++ /dev/null @@ -1,177 +0,0 @@ -// Package legacy implements a DNS provider for solving the DNS-01 challenge using Hetzner DNS. -package legacy - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/hetzner/internal/legacy/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "HETZNER_" - - EnvAPIKey = envNamespace + "API_KEY" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -const minTTL = 60 - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, minTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for hetzner. -// Credentials must be passed in the environment variable: HETZNER_API_KEY. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIKey) - if err != nil { - return nil, fmt.Errorf("hetzner (legacy): %w", err) - } - - config := NewDefaultConfig() - config.APIKey = values[EnvAPIKey] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for hetzner. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("hetzner (legacy): the configuration of the DNS provider is nil") - } - - if config.APIKey == "" { - return nil, errors.New("hetzner (legacy): credentials missing") - } - - if config.TTL < minTTL { - return nil, fmt.Errorf("hetzner (legacy): invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) - } - - client := internal.NewClient(config.APIKey) - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{config: config, client: client}, nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -// Present creates a TXT record to fulfill the dns-01 challenge. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("hetzner (legacy): could not find zone for domain %q: %w", domain, err) - } - - zone := dns01.UnFqdn(authZone) - - ctx := context.Background() - - zoneID, err := d.client.GetZoneID(ctx, zone) - if err != nil { - return fmt.Errorf("hetzner (legacy): %w", err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone) - if err != nil { - return fmt.Errorf("hetzner (legacy): %w", err) - } - - record := internal.DNSRecord{ - Type: "TXT", - Name: subDomain, - Value: info.Value, - TTL: d.config.TTL, - ZoneID: zoneID, - } - - if err := d.client.CreateRecord(ctx, record); err != nil { - return fmt.Errorf("hetzner (legacy): failed to add TXT record: fqdn=%s, zoneID=%s: %w", info.EffectiveFQDN, zoneID, err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("hetzner (legacy): could not find zone for domain %q: %w", domain, err) - } - - zone := dns01.UnFqdn(authZone) - - ctx := context.Background() - - zoneID, err := d.client.GetZoneID(ctx, zone) - if err != nil { - return fmt.Errorf("hetzner (legacy): %w", err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone) - if err != nil { - return fmt.Errorf("hetzner (legacy): %w", err) - } - - record, err := d.client.GetTxtRecord(ctx, subDomain, info.Value, zoneID) - if err != nil { - return fmt.Errorf("hetzner (legacy): %w", err) - } - - if err := d.client.DeleteRecord(ctx, record.ID); err != nil { - return fmt.Errorf("hetzner (legacy): failed to delete TXT record: id=%s, name=%s: %w", record.ID, record.Name, err) - } - - return nil -} diff --git a/providers/dns/hetzner/internal/legacy/hetzner_test.go b/providers/dns/hetzner/internal/legacy/hetzner_test.go deleted file mode 100644 index c9258ecf8..000000000 --- a/providers/dns/hetzner/internal/legacy/hetzner_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package legacy - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest( - EnvAPIKey). - WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAPIKey: "123", - }, - }, - { - desc: "missing credentials", - envVars: map[string]string{ - EnvAPIKey: "", - }, - expected: "hetzner (legacy): some credentials information are missing: HETZNER_API_KEY", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiKey string - ttl int - expected string - }{ - { - desc: "success", - ttl: minTTL, - apiKey: "123", - }, - { - desc: "missing credentials", - ttl: minTTL, - expected: "hetzner (legacy): credentials missing", - }, - { - desc: "invalid TTL", - apiKey: "123", - ttl: 10, - expected: "hetzner (legacy): invalid TTL, TTL (10) must be greater than 60", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIKey = test.apiKey - config.TTL = test.ttl - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/hetzner/internal/legacy/internal/client_test.go b/providers/dns/hetzner/internal/legacy/internal/client_test.go deleted file mode 100644 index ade312a90..000000000 --- a/providers/dns/hetzner/internal/legacy/internal/client_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package internal - -import ( - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder(apiKey string) *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient(apiKey) - client.baseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - With(authHeader, apiKey)) -} - -func TestClient_GetTxtRecord(t *testing.T) { - const zoneID = "zoneA" - - client := mockBuilder("myKeyA"). - Route("GET /api/v1/records", servermock.ResponseFromFixture("get_txt_record.json"), - servermock.CheckQueryParameter().Strict(). - With("zone_id", zoneID)). - Build(t) - - record, err := client.GetTxtRecord(t.Context(), "test1", "txttxttxt", zoneID) - require.NoError(t, err) - - expected := &DNSRecord{ - ID: "1b", - Name: "test1", - Type: "TXT", - Value: "txttxttxt", - Priority: 0, - TTL: 600, - ZoneID: "zoneA", - } - - assert.Equal(t, expected, record) -} - -func TestClient_CreateRecord(t *testing.T) { - const zoneID = "zoneA" - - client := mockBuilder("myKeyB"). - Route("POST /api/v1/records", servermock.ResponseFromFixture("create_txt_record.json"), - servermock.CheckRequestJSONBodyFromFixture("create_txt_record-request.json")). - Build(t) - - record := DNSRecord{ - Name: "test", - Type: "TXT", - Value: "txttxttxt", - TTL: 600, - ZoneID: zoneID, - } - - err := client.CreateRecord(t.Context(), record) - require.NoError(t, err) -} - -func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder("myKeyC"). - Route("DELETE /api/v1/records/recordID", nil). - Build(t) - - err := client.DeleteRecord(t.Context(), "recordID") - require.NoError(t, err) -} - -func TestClient_GetZoneID(t *testing.T) { - client := mockBuilder("myKeyD"). - Route("GET /api/v1/zones", servermock.ResponseFromFixture("get_zone_id.json")). - Build(t) - - zoneID, err := client.GetZoneID(t.Context(), "example.com") - require.NoError(t, err) - - assert.Equal(t, "zoneA", zoneID) -} diff --git a/providers/dns/hetzner/internal/legacy/internal/fixtures/create_txt_record-request.json b/providers/dns/hetzner/internal/legacy/internal/fixtures/create_txt_record-request.json deleted file mode 100644 index 894d81886..000000000 --- a/providers/dns/hetzner/internal/legacy/internal/fixtures/create_txt_record-request.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "test", - "type": "TXT", - "value": "txttxttxt", - "ttl": 600, - "zone_id": "zoneA" -} diff --git a/providers/dns/hetzner/internal/legacy/internal/types.go b/providers/dns/hetzner/internal/types.go similarity index 91% rename from providers/dns/hetzner/internal/legacy/internal/types.go rename to providers/dns/hetzner/internal/types.go index 3b332cc8f..d0e284511 100644 --- a/providers/dns/hetzner/internal/legacy/internal/types.go +++ b/providers/dns/hetzner/internal/types.go @@ -25,12 +25,12 @@ type Zone struct { // Zones a set of DNS zones. type Zones struct { Zones []Zone `json:"zones"` - Meta Meta `json:"meta"` + Meta Meta `json:"meta,omitempty"` } // Meta response metadata. type Meta struct { - Pagination Pagination `json:"pagination"` + Pagination Pagination `json:"pagination,omitempty"` } // Pagination information about pagination. diff --git a/providers/dns/hostingde/hostingde.go b/providers/dns/hostingde/hostingde.go index 1e022b630..db8868965 100644 --- a/providers/dns/hostingde/hostingde.go +++ b/providers/dns/hostingde/hostingde.go @@ -2,12 +2,13 @@ package hostingde import ( + "context" "errors" "fmt" "net/http" + "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/internal/hostingde" @@ -26,18 +27,22 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. -type Config = hostingde.Config +type Config struct { + APIKey string + ZoneName string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - ZoneName: env.GetOrFile(EnvZoneName), TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, @@ -46,7 +51,11 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - prv challenge.ProviderTimeout + config *Config + client *hostingde.Client + + recordIDs map[string]string + recordIDsMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance configured for hosting.de. @@ -60,6 +69,7 @@ func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] + config.ZoneName = env.GetOrFile(EnvZoneName) return NewDNSProviderConfig(config) } @@ -70,36 +80,140 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("hostingde: the configuration of the DNS provider is nil") } - provider, err := hostingde.NewDNSProviderConfig(config, "") - if err != nil { - return nil, fmt.Errorf("hostingde: %w", err) + if config.APIKey == "" { + return nil, errors.New("hostingde: API key missing") } - return &DNSProvider{prv: provider}, nil + return &DNSProvider{ + config: config, + client: hostingde.NewClient(config.APIKey), + recordIDs: make(map[string]string), + }, nil } -// Present creates a TXT record using the specified parameters. +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - err := d.prv.Present(domain, token, keyAuth) + info := dns01.GetChallengeInfo(domain, keyAuth) + + zoneName, err := d.getZoneName(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("hostingde: could not find zone for domain %q: %w", domain, err) + } + + ctx := context.Background() + + // get the ZoneConfig for that domain + zonesFind := hostingde.ZoneConfigsFindRequest{ + Filter: hostingde.Filter{Field: "zoneName", Value: zoneName}, + Limit: 1, + Page: 1, + } + + zoneConfig, err := d.client.GetZone(ctx, zonesFind) if err != nil { return fmt.Errorf("hostingde: %w", err) } + zoneConfig.Name = zoneName + + rec := []hostingde.DNSRecord{{ + Type: "TXT", + Name: dns01.UnFqdn(info.EffectiveFQDN), + Content: info.Value, + TTL: d.config.TTL, + }} + + req := hostingde.ZoneUpdateRequest{ + ZoneConfig: *zoneConfig, + RecordsToAdd: rec, + } + + response, err := d.client.UpdateZone(ctx, req) + if err != nil { + return fmt.Errorf("hostingde: %w", err) + } + + for _, record := range response.Records { + if record.Name == dns01.UnFqdn(info.EffectiveFQDN) && record.Content == fmt.Sprintf(`%q`, info.Value) { + d.recordIDsMu.Lock() + d.recordIDs[info.EffectiveFQDN] = record.ID + d.recordIDsMu.Unlock() + } + } + + if d.recordIDs[info.EffectiveFQDN] == "" { + return fmt.Errorf("hostingde: error getting ID of just created record, for domain %s", domain) + } + return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - err := d.prv.CleanUp(domain, token, keyAuth) + info := dns01.GetChallengeInfo(domain, keyAuth) + + zoneName, err := d.getZoneName(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("hostingde: could not find zone for domain %q: %w", domain, err) + } + + ctx := context.Background() + + // get the ZoneConfig for that domain + zonesFind := hostingde.ZoneConfigsFindRequest{ + Filter: hostingde.Filter{Field: "zoneName", Value: zoneName}, + Limit: 1, + Page: 1, + } + + zoneConfig, err := d.client.GetZone(ctx, zonesFind) if err != nil { return fmt.Errorf("hostingde: %w", err) } + zoneConfig.Name = zoneName + rec := []hostingde.DNSRecord{{ + Type: "TXT", + Name: dns01.UnFqdn(info.EffectiveFQDN), + Content: `"` + info.Value + `"`, + }} + + req := hostingde.ZoneUpdateRequest{ + ZoneConfig: *zoneConfig, + RecordsToDelete: rec, + } + + // Delete record ID from map + d.recordIDsMu.Lock() + delete(d.recordIDs, info.EffectiveFQDN) + d.recordIDsMu.Unlock() + + _, err = d.client.UpdateZone(ctx, req) + if err != nil { + return fmt.Errorf("hostingde: %w", err) + } return nil } -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.prv.Timeout() +func (d *DNSProvider) getZoneName(fqdn string) (string, error) { + if d.config.ZoneName != "" { + return d.config.ZoneName, nil + } + + zoneName, err := dns01.FindZoneByFqdn(fqdn) + if err != nil { + return "", fmt.Errorf("could not find zone: %w", err) + } + + if zoneName == "" { + return "", errors.New("empty zone name") + } + + return dns01.UnFqdn(zoneName), nil } diff --git a/providers/dns/hostingde/hostingde.toml b/providers/dns/hostingde/hostingde.toml index 502a7fe9e..3c0d18f36 100644 --- a/providers/dns/hostingde/hostingde.toml +++ b/providers/dns/hostingde/hostingde.toml @@ -6,7 +6,7 @@ Since = "v1.1.0" Example = ''' HOSTINGDE_API_KEY=xxxxxxxx \ -lego --dns hostingde -d '*.example.com' -d example.com run +lego --email you@example.com --dns hostingde --domains my.example.org run ''' [Configuration] @@ -14,10 +14,10 @@ lego --dns hostingde -d '*.example.com' -d example.com run HOSTINGDE_API_KEY = "API key" [Configuration.Additional] HOSTINGDE_ZONE_NAME = "Zone name in ACE format" - HOSTINGDE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - HOSTINGDE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - HOSTINGDE_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - HOSTINGDE_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + HOSTINGDE_POLLING_INTERVAL = "Time between DNS propagation check" + HOSTINGDE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + HOSTINGDE_TTL = "The TTL of the TXT record used for the DNS challenge" + HOSTINGDE_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.hosting.de/api/#dns" diff --git a/providers/dns/hostingde/hostingde_test.go b/providers/dns/hostingde/hostingde_test.go index a92006f81..d7681f953 100644 --- a/providers/dns/hostingde/hostingde_test.go +++ b/providers/dns/hostingde/hostingde_test.go @@ -49,7 +49,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -59,7 +58,8 @@ func TestNewDNSProvider(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - require.NotNil(t, p.prv) + require.NotNil(t, p.config) + require.NotNil(t, p.recordIDs) } else { require.EqualError(t, err, test.expected) } @@ -101,7 +101,8 @@ func TestNewDNSProviderConfig(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - require.NotNil(t, p.prv) + require.NotNil(t, p.config) + require.NotNil(t, p.recordIDs) } else { require.EqualError(t, err, test.expected) } @@ -115,7 +116,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -129,7 +129,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/hostinger/hostinger.go b/providers/dns/hostinger/hostinger.go deleted file mode 100644 index 13d9ed0f8..000000000 --- a/providers/dns/hostinger/hostinger.go +++ /dev/null @@ -1,211 +0,0 @@ -// Package hostinger implements a DNS provider for solving the DNS-01 challenge using Hostinger. -package hostinger - -import ( - "context" - "errors" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/hostinger/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "HOSTINGER_" - - EnvAPIToken = envNamespace + "API_TOKEN" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIToken string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for Hostinger. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIToken) - if err != nil { - return nil, fmt.Errorf("hostinger: %w", err) - } - - config := NewDefaultConfig() - config.APIToken = values[EnvAPIToken] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Hostinger. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("hostinger: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.APIToken) - if err != nil { - return nil, fmt.Errorf("hostinger: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("hostinger: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("hostinger: %w", err) - } - - ctx := context.Background() - - request := internal.ZoneRequest{ - Overwrite: false, - Zone: []internal.RecordSet{{ - Name: subDomain, - Type: "TXT", - TTL: d.config.TTL, - Records: []internal.Record{ - {Content: info.Value}, - }, - }}, - } - - err = d.client.UpdateDNSRecords(ctx, dns01.UnFqdn(authZone), request) - if err != nil { - return fmt.Errorf("hostinger: update DNS records (add): %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("hostinger: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("hostinger: %w", err) - } - - ctx := context.Background() - - recordSet, err := d.findRecordSet(ctx, authZone, subDomain) - if err != nil { - return fmt.Errorf("hostinger: %w", err) - } - - var newRecords []internal.Record - - for _, record := range recordSet.Records { - if record.Content == info.Value || record.Content == strconv.Quote(info.Value) { - continue - } - - newRecords = append(newRecords, record) - } - - recordSet.Records = newRecords - - if len(recordSet.Records) > 0 { - request := internal.ZoneRequest{ - Overwrite: true, - Zone: []internal.RecordSet{recordSet}, - } - - err = d.client.UpdateDNSRecords(ctx, dns01.UnFqdn(authZone), request) - if err != nil { - return fmt.Errorf("hostinger: update DNS records (delete): %w", err) - } - - return nil - } - - filters := []internal.Filter{{ - Name: subDomain, - Type: "TXT", - }} - - err = d.client.DeleteDNSRecords(ctx, dns01.UnFqdn(authZone), filters) - if err != nil { - return fmt.Errorf("hostinger: delete DNS records: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -func (d *DNSProvider) findRecordSet(ctx context.Context, authZone, subDomain string) (internal.RecordSet, error) { - recordSets, err := d.client.GetDNSRecords(ctx, dns01.UnFqdn(authZone)) - if err != nil { - return internal.RecordSet{}, fmt.Errorf("get DNS records: %w", err) - } - - for _, recordSet := range recordSets { - if recordSet.Name != subDomain || recordSet.Type != "TXT" { - continue - } - - return recordSet, nil - } - - return internal.RecordSet{}, fmt.Errorf("no record found for domain %q and subdomain %q", authZone, subDomain) -} diff --git a/providers/dns/hostinger/hostinger.toml b/providers/dns/hostinger/hostinger.toml deleted file mode 100644 index a6f152e73..000000000 --- a/providers/dns/hostinger/hostinger.toml +++ /dev/null @@ -1,22 +0,0 @@ -Name = "Hostinger" -Description = '''''' -URL = "https://www.hostinger.com/" -Code = "hostinger" -Since = "v4.27.0" - -Example = ''' -HOSTINGER_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns hostinger -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - HOSTINGER_API_TOKEN = "API Token" - [Configuration.Additional] - HOSTINGER_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - HOSTINGER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - HOSTINGER_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - HOSTINGER_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://developers.hostinger.com/#tag/dns-zone" diff --git a/providers/dns/hostinger/hostinger_test.go b/providers/dns/hostinger/hostinger_test.go deleted file mode 100644 index 90ecba529..000000000 --- a/providers/dns/hostinger/hostinger_test.go +++ /dev/null @@ -1,180 +0,0 @@ -package hostinger - -import ( - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAPIToken).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAPIToken: "secret", - }, - }, - { - desc: "missing API token", - envVars: map[string]string{ - EnvAPIToken: "", - }, - expected: "hostinger: some credentials information are missing: HOSTINGER_API_TOKEN", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiToken string - expected string - }{ - { - desc: "success", - apiToken: "secret", - }, - { - desc: "missing API token", - expected: "hostinger: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIToken = test.apiToken - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIToken = "secret" - config.HTTPClient = server.Client() - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - p.client.BaseURL, _ = url.Parse(server.URL) - - return p, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(). - WithAuthorization("Bearer secret"), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("PUT /api/dns/v1/zones/example.com", - servermock.ResponseFromInternal("update_dns_records.json"), - servermock.CheckRequestJSONBodyFromInternal("update_dns_records-request.json")). - Build(t) - - err := provider.Present("example.com", "", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp_update(t *testing.T) { - provider := mockBuilder(). - Route("GET /api/dns/v1/zones/example.com", - servermock.ResponseFromInternal("get_dns_records_acme.json")). - Route("PUT /api/dns/v1/zones/example.com", - servermock.ResponseFromInternal("update_dns_records.json"), - servermock.CheckRequestJSONBodyFromInternal("update_dns_records_base-request.json")). - Build(t) - - err := provider.CleanUp("example.com", "", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp_delete(t *testing.T) { - provider := mockBuilder(). - Route("GET /api/dns/v1/zones/example.com", - servermock.ResponseFromInternal("get_dns_records_empty.json")). - Route("DELETE /api/dns/v1/zones/example.com", - servermock.ResponseFromInternal("delete_dns_records.json"), - servermock.CheckRequestJSONBody(`{"filters":[{"name":"_acme-challenge","type":"TXT"}]}`)). - Build(t) - - err := provider.CleanUp("example.com", "", "123d==") - require.NoError(t, err) -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/hostinger/internal/client.go b/providers/dns/hostinger/internal/client.go deleted file mode 100644 index 9da712d61..000000000 --- a/providers/dns/hostinger/internal/client.go +++ /dev/null @@ -1,156 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" -) - -const defaultBaseURL = "https://developers.hostinger.com" - -const authorizationHeader = "Authorization" - -// Client the Hostinger API client. -type Client struct { - token string - - BaseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(token string) (*Client, error) { - if token == "" { - return nil, errors.New("credentials missing") - } - - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - token: token, - BaseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -// GetDNSRecords retrieves DNS zone records for a specific domain. -// https://developers.hostinger.com/#tag/dns-zone/get/api/dns/v1/zones/{domain} -func (c *Client) GetDNSRecords(ctx context.Context, domain string) ([]RecordSet, error) { - endpoint := c.BaseURL.JoinPath("/api/dns/v1/zones/", domain) - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var result []RecordSet - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return result, nil -} - -// UpdateDNSRecords updates DNS records for the selected domain. -// https://developers.hostinger.com/#tag/dns-zone/put/api/dns/v1/zones/{domain} -func (c *Client) UpdateDNSRecords(ctx context.Context, domain string, zone ZoneRequest) error { - endpoint := c.BaseURL.JoinPath("/api/dns/v1/zones/", domain) - - req, err := newJSONRequest(ctx, http.MethodPut, endpoint, zone) - if err != nil { - return err - } - - return c.do(req, nil) -} - -// DeleteDNSRecords deletes DNS records for the selected domain. -// https://developers.hostinger.com/#tag/dns-zone/delete/api/dns/v1/zones/{domain} -func (c *Client) DeleteDNSRecords(ctx context.Context, domain string, filters []Filter) error { - endpoint := c.BaseURL.JoinPath("/api/dns/v1/zones/", domain) - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, Filters{Filters: filters}) - if err != nil { - return err - } - - return c.do(req, nil) -} - -func (c *Client) do(req *http.Request, result any) error { - req.Header.Set(authorizationHeader, "Bearer "+c.token) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIError - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return &errAPI -} diff --git a/providers/dns/hostinger/internal/client_test.go b/providers/dns/hostinger/internal/client_test.go deleted file mode 100644 index 69cab5587..000000000 --- a/providers/dns/hostinger/internal/client_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("secret") - if err != nil { - return nil, err - } - - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - With("Authorization", "Bearer secret"), - ) -} - -func TestClient_GetDNSRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /api/dns/v1/zones/example.com", - servermock.ResponseFromFixture("get_dns_records.json")). - Build(t) - - records, err := client.GetDNSRecords(t.Context(), "example.com") - require.NoError(t, err) - - expected := []RecordSet{ - { - Name: "_acme-challenge", - Records: []Record{{ - Content: "aaa", - }}, - TTL: 14400, - Type: "TXT", - }, - { - Name: "_acme-challenge", - Records: []Record{{ - Content: "example.com.", - }}, - TTL: 14400, - Type: "A", - }, - } - - assert.Equal(t, expected, records) -} - -func TestClient_GetDNSRecords_error(t *testing.T) { - client := mockBuilder(). - Route("GET /api/dns/v1/zones/example.com", - servermock.ResponseFromFixture("error_401.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) - - _, err := client.GetDNSRecords(t.Context(), "example.com") - - require.EqualError(t, err, "26a91bd9-f8c8-4a83-9df9-83e23d696fe3: Unauthenticated") -} - -func TestClient_UpdateDNSRecords(t *testing.T) { - client := mockBuilder(). - Route("PUT /api/dns/v1/zones/example.com", - servermock.ResponseFromFixture("update_dns_records.json"), - servermock.CheckRequestJSONBodyFromFixture("update_dns_records-request.json")). - Build(t) - - zone := ZoneRequest{ - Overwrite: false, - Zone: []RecordSet{ - { - Name: "_acme-challenge", - Records: []Record{ - {Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"}, - }, - TTL: 120, - Type: "TXT", - }, - }, - } - - err := client.UpdateDNSRecords(t.Context(), "example.com", zone) - require.NoError(t, err) -} - -func TestClient_UpdateDNSRecords_error(t *testing.T) { - client := mockBuilder(). - Route("PUT /api/dns/v1/zones/example.com", - servermock.ResponseFromFixture("error_422.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) - - zone := ZoneRequest{ - Zone: []RecordSet{{ - Name: "_acme-challenge", - Records: []Record{{ - Content: "aaa", - }}, - TTL: 14400, - Type: "TXT", - }}, - } - - err := client.UpdateDNSRecords(t.Context(), "example.com", zone) - - require.EqualError(t, err, "26a91bd9-f8c8-4a83-9df9-83e23d696fe3: The name field is required. (and 1 more error): field_1: The field_1 field is required., The field_1 must be a number.") -} - -func TestClient_DeleteDNSRecords(t *testing.T) { - client := mockBuilder(). - Route("DELETE /api/dns/v1/zones/example.com", - servermock.ResponseFromFixture("delete_dns_records.json"), - servermock.CheckRequestJSONBody(`{"filters":[{"name":"_acme-challenge","type":"TXT"}]}`)). - Build(t) - - filters := []Filter{{ - Name: "_acme-challenge", - Type: "TXT", - }} - - err := client.DeleteDNSRecords(t.Context(), "example.com", filters) - require.NoError(t, err) -} - -func TestClient_DeleteDNSRecords_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /api/dns/v1/zones/example.com", - servermock.ResponseFromFixture("error_401.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) - - filters := []Filter{{ - Name: "_acme-challenge", - Type: "TXT", - }} - - err := client.DeleteDNSRecords(t.Context(), "example.com", filters) - - require.EqualError(t, err, "26a91bd9-f8c8-4a83-9df9-83e23d696fe3: Unauthenticated") -} diff --git a/providers/dns/hostinger/internal/fixtures/delete_dns_records.json b/providers/dns/hostinger/internal/fixtures/delete_dns_records.json deleted file mode 100644 index 11d2582b4..000000000 --- a/providers/dns/hostinger/internal/fixtures/delete_dns_records.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "message": "Request accepted" -} diff --git a/providers/dns/hostinger/internal/fixtures/error_401.json b/providers/dns/hostinger/internal/fixtures/error_401.json deleted file mode 100644 index 1b7381ff6..000000000 --- a/providers/dns/hostinger/internal/fixtures/error_401.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "message": "Unauthenticated", - "correlation_id": "26a91bd9-f8c8-4a83-9df9-83e23d696fe3" -} diff --git a/providers/dns/hostinger/internal/fixtures/error_422.json b/providers/dns/hostinger/internal/fixtures/error_422.json deleted file mode 100644 index 6ec286823..000000000 --- a/providers/dns/hostinger/internal/fixtures/error_422.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "message": "The name field is required. (and 1 more error)", - "errors": { - "field_1": [ - "The field_1 field is required.", - "The field_1 must be a number." - ] - }, - "correlation_id": "26a91bd9-f8c8-4a83-9df9-83e23d696fe3" -} diff --git a/providers/dns/hostinger/internal/fixtures/get_dns_records.json b/providers/dns/hostinger/internal/fixtures/get_dns_records.json deleted file mode 100644 index e51edd4dc..000000000 --- a/providers/dns/hostinger/internal/fixtures/get_dns_records.json +++ /dev/null @@ -1,24 +0,0 @@ -[ - { - "name": "_acme-challenge", - "records": [ - { - "content": "aaa", - "is_disabled": false - } - ], - "ttl": 14400, - "type": "TXT" - }, - { - "name": "_acme-challenge", - "records": [ - { - "content": "example.com.", - "is_disabled": false - } - ], - "ttl": 14400, - "type": "A" - } -] diff --git a/providers/dns/hostinger/internal/fixtures/get_dns_records_acme.json b/providers/dns/hostinger/internal/fixtures/get_dns_records_acme.json deleted file mode 100644 index 99a574514..000000000 --- a/providers/dns/hostinger/internal/fixtures/get_dns_records_acme.json +++ /dev/null @@ -1,27 +0,0 @@ -[ - { - "name": "_acme-challenge", - "records": [ - { - "content": "aaa", - "is_disabled": false - }, - { - "content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" - } - ], - "ttl": 14400, - "type": "TXT" - }, - { - "name": "_acme-challenge", - "records": [ - { - "content": "example.com.", - "is_disabled": false - } - ], - "ttl": 14400, - "type": "A" - } -] diff --git a/providers/dns/hostinger/internal/fixtures/get_dns_records_empty.json b/providers/dns/hostinger/internal/fixtures/get_dns_records_empty.json deleted file mode 100644 index 9989a3fc4..000000000 --- a/providers/dns/hostinger/internal/fixtures/get_dns_records_empty.json +++ /dev/null @@ -1,23 +0,0 @@ -[ - { - "name": "_acme-challenge", - "records": [ - { - "content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" - } - ], - "ttl": 14400, - "type": "TXT" - }, - { - "name": "_acme-challenge", - "records": [ - { - "content": "example.com.", - "is_disabled": false - } - ], - "ttl": 14400, - "type": "A" - } -] diff --git a/providers/dns/hostinger/internal/fixtures/update_dns_records-request.json b/providers/dns/hostinger/internal/fixtures/update_dns_records-request.json deleted file mode 100644 index 6f287b3fc..000000000 --- a/providers/dns/hostinger/internal/fixtures/update_dns_records-request.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "overwrite": false, - "zone": [ - { - "name": "_acme-challenge", - "records": [ - { - "content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" - } - ], - "ttl": 120, - "type": "TXT" - } - ] -} diff --git a/providers/dns/hostinger/internal/fixtures/update_dns_records.json b/providers/dns/hostinger/internal/fixtures/update_dns_records.json deleted file mode 100644 index 11d2582b4..000000000 --- a/providers/dns/hostinger/internal/fixtures/update_dns_records.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "message": "Request accepted" -} diff --git a/providers/dns/hostinger/internal/fixtures/update_dns_records_base-request.json b/providers/dns/hostinger/internal/fixtures/update_dns_records_base-request.json deleted file mode 100644 index c42ddc6d7..000000000 --- a/providers/dns/hostinger/internal/fixtures/update_dns_records_base-request.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "overwrite": true, - "zone": [ - { - "name": "_acme-challenge", - "records": [ - { - "content": "aaa" - } - ], - "ttl": 14400, - "type": "TXT" - } - ] -} diff --git a/providers/dns/hostinger/internal/types.go b/providers/dns/hostinger/internal/types.go deleted file mode 100644 index c1a02ff8c..000000000 --- a/providers/dns/hostinger/internal/types.go +++ /dev/null @@ -1,50 +0,0 @@ -package internal - -import ( - "fmt" - "strings" -) - -type APIError struct { - Message string `json:"message,omitempty"` - Errors map[string][]string `json:"errors,omitempty"` - CorrelationID string `json:"correlation_id,omitempty"` -} - -func (a *APIError) Error() string { - msg := new(strings.Builder) - - _, _ = fmt.Fprintf(msg, "%s: %s", a.CorrelationID, a.Message) - - for field, values := range a.Errors { - _, _ = fmt.Fprintf(msg, ": %s: %s", field, strings.Join(values, ", ")) - } - - return msg.String() -} - -type ZoneRequest struct { - Overwrite bool `json:"overwrite"` - Zone []RecordSet `json:"zone,omitempty"` -} - -type RecordSet struct { - Name string `json:"name,omitempty"` - Records []Record `json:"records,omitempty"` - TTL int `json:"ttl,omitempty"` - Type string `json:"type,omitempty"` -} - -type Record struct { - Content string `json:"content,omitempty"` - IsDisabled bool `json:"is_disabled,omitempty"` -} - -type Filters struct { - Filters []Filter `json:"filters"` -} - -type Filter struct { - Name string `json:"name"` - Type string `json:"type"` -} diff --git a/providers/dns/hostingnl/hostingnl.go b/providers/dns/hostingnl/hostingnl.go deleted file mode 100644 index a49941817..000000000 --- a/providers/dns/hostingnl/hostingnl.go +++ /dev/null @@ -1,168 +0,0 @@ -// Package hostingnl implements a DNS provider for solving the DNS-01 challenge using hosting.nl. -package hostingnl - -import ( - "context" - "errors" - "fmt" - "net/http" - "strconv" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/hostingnl/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Environment variables names. -const ( - envNamespace = "HOSTINGNL_" - - EnvAPIKey = envNamespace + "API_KEY" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - HTTPClient *http.Client - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client - - recordIDs map[string]string - recordIDsMu sync.Mutex -} - -// NewDNSProvider returns a DNSProvider instance configured for hosting.nl. -// Credentials must be passed in the environment variables: -// HOSTINGNL_APIKEY. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIKey) - if err != nil { - return nil, fmt.Errorf("hostingnl: %w", err) - } - - config := NewDefaultConfig() - config.APIKey = values[EnvAPIKey] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for hosting.nl. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("hostingnl: the configuration of the DNS provider is nil") - } - - if config.APIKey == "" { - return nil, errors.New("hostingnl: APIKey is missing") - } - - client := internal.NewClient(config.APIKey) - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - recordIDs: make(map[string]string), - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("hostingnl: could not find zone for domain %q: %w", domain, err) - } - - record := internal.Record{ - Name: dns01.UnFqdn(info.EffectiveFQDN), - Type: "TXT", - Content: strconv.Quote(info.Value), - TTL: d.config.TTL, - Priority: 0, - } - - newRecord, err := d.client.AddRecord(context.Background(), dns01.UnFqdn(authZone), record) - if err != nil { - return fmt.Errorf("hostingnl: failed to create TXT record, fqdn=%s: %w", info.EffectiveFQDN, err) - } - - d.recordIDsMu.Lock() - d.recordIDs[token] = newRecord.ID - d.recordIDsMu.Unlock() - - return nil -} - -// CleanUp removes the TXT records matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("hostingnl: could not find zone for domain %q: %w", domain, err) - } - - // gets the record's unique ID - d.recordIDsMu.Lock() - recordID, ok := d.recordIDs[token] - d.recordIDsMu.Unlock() - - if !ok { - return fmt.Errorf("hostingnl: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) - } - - err = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), recordID) - if err != nil { - return fmt.Errorf("hostingnl: failed to delete TXT record, id=%s: %w", recordID, err) - } - - // deletes record ID from map - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/hostingnl/hostingnl.toml b/providers/dns/hostingnl/hostingnl.toml deleted file mode 100644 index 943264ed3..000000000 --- a/providers/dns/hostingnl/hostingnl.toml +++ /dev/null @@ -1,22 +0,0 @@ -Name = "Hosting.nl" -Description = '''''' -URL = "https://hosting.nl" -Code = "hostingnl" -Since = "v4.30.0" - -Example = ''' -HOSTINGNL_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns hostingnl -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - HOSTINGNL_API_KEY = "The API key" - [Configuration.Additional] - HOSTINGNL_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - HOSTINGNL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - HOSTINGNL_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - HOSTINGNL_HTTP_TIMEOUT = "API request timeout in seconds (Default: 10)" - -[Links] - API = "https://api.hosting.nl/api/documentation" diff --git a/providers/dns/hostingnl/hostingnl_test.go b/providers/dns/hostingnl/hostingnl_test.go deleted file mode 100644 index cef754c7c..000000000 --- a/providers/dns/hostingnl/hostingnl_test.go +++ /dev/null @@ -1,167 +0,0 @@ -package hostingnl - -import ( - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAPIKey: "key", - }, - }, - { - desc: "missing API key", - envVars: map[string]string{}, - expected: "hostingnl: some credentials information are missing: HOSTINGNL_API_KEY", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiKey string - expected string - }{ - { - desc: "success", - apiKey: "key", - }, - { - desc: "missing API key", - expected: "hostingnl: APIKey is missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIKey = test.apiKey - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIKey = "secret" - config.HTTPClient = server.Client() - - provider, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - provider.client.BaseURL, _ = url.Parse(server.URL) - - return provider, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(). - With("API-TOKEN", "secret"), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("POST /domains/example.com/dns", - servermock.ResponseFromInternal("add_record.json"), - servermock.CheckQueryParameter().Strict(), - servermock.CheckRequestJSONBodyFromInternal("add_record-request.json")). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("DELETE /domains/example.com/dns", - servermock.ResponseFromInternal("delete_record.json"), - servermock.CheckQueryParameter().Strict(), - servermock.CheckRequestJSONBodyFromInternal("delete_record-request.json")). - Build(t) - - provider.recordIDs["abc"] = "12345" - - err := provider.CleanUp("example.com", "abc", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/hostingnl/internal/client.go b/providers/dns/hostingnl/internal/client.go deleted file mode 100644 index f2d7b5346..000000000 --- a/providers/dns/hostingnl/internal/client.go +++ /dev/null @@ -1,144 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" -) - -const defaultBaseURL = "https://api.hosting.nl" - -type Client struct { - apiKey string - - BaseURL *url.URL - HTTPClient *http.Client -} - -func NewClient(apiKey string) *Client { - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - apiKey: apiKey, - BaseURL: baseURL, - HTTPClient: &http.Client{Timeout: 5 * time.Second}, - } -} - -func (c Client) AddRecord(ctx context.Context, domain string, record Record) (*Record, error) { - endpoint := c.BaseURL.JoinPath("domains", domain, "dns") - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, []Record{record}) - if err != nil { - return nil, err - } - - var result APIResponse[Record] - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - if len(result.Data) != 1 { - return nil, fmt.Errorf("unexpected response data: %v", result.Data) - } - - return &result.Data[0], nil -} - -func (c Client) DeleteRecord(ctx context.Context, domain, recordID string) error { - endpoint := c.BaseURL.JoinPath("domains", domain, "dns") - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, []Record{{ID: recordID}}) - if err != nil { - return err - } - - var result APIResponse[Record] - - err = c.do(req, &result) - if err != nil { - return err - } - - return nil -} - -func (c Client) do(req *http.Request, result any) error { - useragent.SetHeader(req.Header) - - req.Header.Set("API-TOKEN", c.apiKey) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var apiErr APIError - - err := json.Unmarshal(raw, &apiErr) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return fmt.Errorf("[status code: %d] %w", resp.StatusCode, apiErr) -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} diff --git a/providers/dns/hostingnl/internal/client_test.go b/providers/dns/hostingnl/internal/client_test.go deleted file mode 100644 index efdb98980..000000000 --- a/providers/dns/hostingnl/internal/client_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package internal - -import ( - "context" - "net/http" - "net/http/httptest" - "net/url" - "strconv" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder( - func(server *httptest.Server) (*Client, error) { - client := NewClient("secret") - client.HTTPClient = server.Client() - client.BaseURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(). - With("API-TOKEN", "secret"), - ) -} - -func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/example.com/dns", - servermock.ResponseFromFixture("add_record.json"), - servermock.CheckQueryParameter().Strict(), - servermock.CheckRequestJSONBodyFromFixture("add_record-request.json")). - Build(t) - - record := Record{ - Name: "_acme-challenge.example.com", - Type: "TXT", - Content: strconv.Quote("ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"), - TTL: 120, - } - - newRecord, err := client.AddRecord(context.Background(), "example.com", record) - require.NoError(t, err) - - expected := &Record{ - ID: "12345", - Name: "_acme-challenge.example.com", - Type: "TXT", - Content: strconv.Quote("ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"), - TTL: 120, - } - - assert.Equal(t, expected, newRecord) -} - -func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /domains/example.com/dns", - servermock.ResponseFromFixture("delete_record.json"), - servermock.CheckQueryParameter().Strict(), - servermock.CheckRequestJSONBodyFromFixture("delete_record-request.json")). - Build(t) - - err := client.DeleteRecord(context.Background(), "example.com", "12345") - require.NoError(t, err) -} - -func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /domains/example.com/dns", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) - - err := client.DeleteRecord(context.Background(), "example.com", "12345") - require.EqualError(t, err, "[status code: 401] Something went wrong") -} - -func TestClient_DeleteRecord_error_other(t *testing.T) { - client := mockBuilder(). - Route("DELETE /domains/example.com/dns", - servermock.ResponseFromFixture("error_other.json"). - WithStatusCode(http.StatusNotFound)). - Build(t) - - err := client.DeleteRecord(context.Background(), "example.com", "12345") - require.EqualError(t, err, "[status code: 404] Resource not found") -} diff --git a/providers/dns/hostingnl/internal/fixtures/add_record-request.json b/providers/dns/hostingnl/internal/fixtures/add_record-request.json deleted file mode 100644 index 6b68ec3c6..000000000 --- a/providers/dns/hostingnl/internal/fixtures/add_record-request.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - { - "name": "_acme-challenge.example.com", - "type": "TXT", - "content": "\"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"", - "ttl": 120 - } -] diff --git a/providers/dns/hostingnl/internal/fixtures/add_record.json b/providers/dns/hostingnl/internal/fixtures/add_record.json deleted file mode 100644 index a822a4f8d..000000000 --- a/providers/dns/hostingnl/internal/fixtures/add_record.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "success": true, - "data": [ - { - "id": "12345", - "type": "TXT", - "content": "\"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"", - "name": "_acme-challenge.example.com", - "prio": 0, - "ttl": 120 - } - ] -} diff --git a/providers/dns/hostingnl/internal/fixtures/delete_record-request.json b/providers/dns/hostingnl/internal/fixtures/delete_record-request.json deleted file mode 100644 index cfc26d2b9..000000000 --- a/providers/dns/hostingnl/internal/fixtures/delete_record-request.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - { - "id": "12345" - } -] diff --git a/providers/dns/hostingnl/internal/fixtures/delete_record.json b/providers/dns/hostingnl/internal/fixtures/delete_record.json deleted file mode 100644 index c041c1f6d..000000000 --- a/providers/dns/hostingnl/internal/fixtures/delete_record.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "success": true, - "data": [ - { - "id": "12345" - } - ] -} diff --git a/providers/dns/hostingnl/internal/fixtures/error.json b/providers/dns/hostingnl/internal/fixtures/error.json deleted file mode 100644 index 170587246..000000000 --- a/providers/dns/hostingnl/internal/fixtures/error.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "errors": { - "message": "Something went wrong" - } -} diff --git a/providers/dns/hostingnl/internal/fixtures/error_other.json b/providers/dns/hostingnl/internal/fixtures/error_other.json deleted file mode 100644 index ca7ecab28..000000000 --- a/providers/dns/hostingnl/internal/fixtures/error_other.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "error": "Resource not found" -} diff --git a/providers/dns/hostingnl/internal/types.go b/providers/dns/hostingnl/internal/types.go deleted file mode 100644 index f324665fe..000000000 --- a/providers/dns/hostingnl/internal/types.go +++ /dev/null @@ -1,36 +0,0 @@ -package internal - -type Record struct { - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` - Content string `json:"content,omitempty"` - TTL int `json:"ttl,omitempty"` - Priority int `json:"prio,omitempty"` -} - -type APIResponse[T any] struct { - Success bool `json:"success"` - Data []T `json:"data"` -} - -type APIError struct { - ErrorMsg string `json:"error"` - Errors Error `json:"errors"` -} - -func (e APIError) Error() string { - if e.ErrorMsg != "" { - return e.ErrorMsg - } - - return e.Errors.Error() -} - -type Error struct { - Message string `json:"message"` -} - -func (e Error) Error() string { - return e.Message -} diff --git a/providers/dns/hosttech/hosttech.go b/providers/dns/hosttech/hosttech.go index 73346f6cb..94a6a0795 100644 --- a/providers/dns/hosttech/hosttech.go +++ b/providers/dns/hosttech/hosttech.go @@ -10,11 +10,9 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/hosttech/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -29,8 +27,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string @@ -85,11 +81,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("hosttech: missing credentials") } - client := internal.NewClient( - clientdebug.Wrap( - internal.OAuthStaticAccessToken(config.HTTPClient, config.APIKey), - ), - ) + client := internal.NewClient(internal.OAuthStaticAccessToken(config.HTTPClient, config.APIKey)) return &DNSProvider{ config: config, @@ -164,7 +156,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() - if !ok { return fmt.Errorf("hosttech: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) } @@ -174,9 +165,5 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("hosttech: %w", err) } - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - return nil } diff --git a/providers/dns/hosttech/hosttech.toml b/providers/dns/hosttech/hosttech.toml index 52c01fd31..b50eaeed9 100644 --- a/providers/dns/hosttech/hosttech.toml +++ b/providers/dns/hosttech/hosttech.toml @@ -6,7 +6,7 @@ Since = "v4.5.0" Example = ''' HOSTTECH_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --dns hosttech -d '*.example.com' -d example.com run +lego --email you@example.com --dns hosttech --domains my.example.org run ''' [Configuration] @@ -14,10 +14,10 @@ lego --dns hosttech -d '*.example.com' -d example.com run HOSTTECH_API_KEY = "API login" HOSTTECH_PASSWORD = "API password" [Configuration.Additional] - HOSTTECH_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - HOSTTECH_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - HOSTTECH_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)" - HOSTTECH_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + HOSTTECH_POLLING_INTERVAL = "Time between DNS propagation check" + HOSTTECH_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + HOSTTECH_TTL = "The TTL of the TXT record used for the DNS challenge" + HOSTTECH_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://api.ns1.hosttech.eu/api/documentation" diff --git a/providers/dns/hosttech/hosttech_test.go b/providers/dns/hosttech/hosttech_test.go index 042b73353..6f0d0bd3e 100644 --- a/providers/dns/hosttech/hosttech_test.go +++ b/providers/dns/hosttech/hosttech_test.go @@ -33,7 +33,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -93,7 +92,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -107,7 +105,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/hosttech/internal/client.go b/providers/dns/hosttech/internal/client.go index 557d54298..78b594558 100644 --- a/providers/dns/hosttech/internal/client.go +++ b/providers/dns/hosttech/internal/client.go @@ -36,7 +36,7 @@ func NewClient(hc *http.Client) *Client { // GetZones Get a list of all zones. // https://api.ns1.hosttech.eu/api/documentation/#/Zones/get_api_user_v1_zones -func (c *Client) GetZones(ctx context.Context, query string, limit, offset int) ([]Zone, error) { +func (c Client) GetZones(ctx context.Context, query string, limit, offset int) ([]Zone, error) { endpoint := c.baseURL.JoinPath("user", "v1", "zones") values := endpoint.Query() @@ -58,7 +58,6 @@ func (c *Client) GetZones(ctx context.Context, query string, limit, offset int) } result := apiResponse[[]Zone]{} - err = c.do(req, &result) if err != nil { return nil, err @@ -69,7 +68,7 @@ func (c *Client) GetZones(ctx context.Context, query string, limit, offset int) // GetZone Get a single zone. // https://api.ns1.hosttech.eu/api/documentation/#/Zones/get_api_user_v1_zones__zoneId_ -func (c *Client) GetZone(ctx context.Context, zoneID string) (*Zone, error) { +func (c Client) GetZone(ctx context.Context, zoneID string) (*Zone, error) { endpoint := c.baseURL.JoinPath("user", "v1", "zones", zoneID) req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) @@ -78,7 +77,6 @@ func (c *Client) GetZone(ctx context.Context, zoneID string) (*Zone, error) { } result := apiResponse[*Zone]{} - err = c.do(req, &result) if err != nil { return nil, err @@ -89,7 +87,7 @@ func (c *Client) GetZone(ctx context.Context, zoneID string) (*Zone, error) { // GetRecords Returns a list of all records for the given zone. // https://api.ns1.hosttech.eu/api/documentation/#/Records/get_api_user_v1_zones__zoneId__records -func (c *Client) GetRecords(ctx context.Context, zoneID, recordType string) ([]Record, error) { +func (c Client) GetRecords(ctx context.Context, zoneID, recordType string) ([]Record, error) { endpoint := c.baseURL.JoinPath("user", "v1", "zones", zoneID, "records") values := endpoint.Query() @@ -106,7 +104,6 @@ func (c *Client) GetRecords(ctx context.Context, zoneID, recordType string) ([]R } result := apiResponse[[]Record]{} - err = c.do(req, &result) if err != nil { return nil, err @@ -117,7 +114,7 @@ func (c *Client) GetRecords(ctx context.Context, zoneID, recordType string) ([]R // AddRecord Adds a new record to the zone and returns the newly created record. // https://api.ns1.hosttech.eu/api/documentation/#/Records/post_api_user_v1_zones__zoneId__records -func (c *Client) AddRecord(ctx context.Context, zoneID string, record Record) (*Record, error) { +func (c Client) AddRecord(ctx context.Context, zoneID string, record Record) (*Record, error) { endpoint := c.baseURL.JoinPath("user", "v1", "zones", zoneID, "records") req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) @@ -126,7 +123,6 @@ func (c *Client) AddRecord(ctx context.Context, zoneID string, record Record) (* } result := apiResponse[*Record]{} - err = c.do(req, &result) if err != nil { return nil, err @@ -137,7 +133,7 @@ func (c *Client) AddRecord(ctx context.Context, zoneID string, record Record) (* // DeleteRecord Deletes a single record for the given id. // https://api.ns1.hosttech.eu/api/documentation/#/Records/delete_api_user_v1_zones__zoneId__records__recordId_ -func (c *Client) DeleteRecord(ctx context.Context, zoneID, recordID string) error { +func (c Client) DeleteRecord(ctx context.Context, zoneID, recordID string) error { endpoint := c.baseURL.JoinPath("user", "v1", "zones", zoneID, "records", recordID) req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) @@ -148,7 +144,7 @@ func (c *Client) DeleteRecord(ctx context.Context, zoneID, recordID string) erro return c.do(req, nil) } -func (c *Client) do(req *http.Request, result any) error { +func (c Client) do(req *http.Request, result any) error { resp, errD := c.httpClient.Do(req) if errD != nil { return errutils.NewHTTPDoError(req, errD) @@ -206,7 +202,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) errAPI := &APIError{StatusCode: resp.StatusCode} - err := json.Unmarshal(raw, errAPI) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/hosttech/internal/client_test.go b/providers/dns/hosttech/internal/client_test.go index 223a0d9cf..bf90acc9f 100644 --- a/providers/dns/hosttech/internal/client_test.go +++ b/providers/dns/hosttech/internal/client_test.go @@ -1,40 +1,26 @@ package internal import ( + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const testAPIKey = "secret" -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient(OAuthStaticAccessToken(server.Client(), testAPIKey)) - client.baseURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("Bearer secret")) -} - func TestClient_GetZones(t *testing.T) { - client := mockBuilder(). - Route("GET /user/v1/zones", - servermock.ResponseFromFixture("zones.json"), - servermock.CheckQueryParameter().Strict(). - With("limit", "100"). - With("query", "")). - Build(t) + client := setupTest(t, "/user/v1/zones", testHandler(http.MethodGet, http.StatusOK, "zones.json")) - zones, err := client.GetZones(t.Context(), "", 100, 0) + zones, err := client.GetZones(context.Background(), "", 100, 0) require.NoError(t, err) expected := []Zone{ @@ -53,23 +39,16 @@ func TestClient_GetZones(t *testing.T) { } func TestClient_GetZones_error(t *testing.T) { - client := mockBuilder(). - Route("GET /user/v1/zones", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client := setupTest(t, "/user/v1/zones", testHandler(http.MethodGet, http.StatusUnauthorized, "error.json")) - _, err := client.GetZones(t.Context(), "", 100, 0) + _, err := client.GetZones(context.Background(), "", 100, 0) require.Error(t, err) } func TestClient_GetZone(t *testing.T) { - client := mockBuilder(). - Route("GET /user/v1/zones/123", - servermock.ResponseFromFixture("zone.json")). - Build(t) + client := setupTest(t, "/user/v1/zones/123", testHandler(http.MethodGet, http.StatusOK, "zone.json")) - zone, err := client.GetZone(t.Context(), "123") + zone, err := client.GetZone(context.Background(), "123") require.NoError(t, err) expected := &Zone{ @@ -86,25 +65,16 @@ func TestClient_GetZone(t *testing.T) { } func TestClient_GetZone_error(t *testing.T) { - client := mockBuilder(). - Route("GET /user/v1/zones/123", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client := setupTest(t, "/user/v1/zones/123", testHandler(http.MethodGet, http.StatusUnauthorized, "error.json")) - _, err := client.GetZone(t.Context(), "123") - require.EqualError(t, err, "401: Unauthenticated.") + _, err := client.GetZone(context.Background(), "123") + require.Error(t, err) } func TestClient_GetRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /user/v1/zones/123/records", - servermock.ResponseFromFixture("records.json"), - servermock.CheckQueryParameter().Strict(). - With("type", "TXT")). - Build(t) + client := setupTest(t, "/user/v1/zones/123/records", testHandler(http.MethodGet, http.StatusOK, "records.json")) - records, err := client.GetRecords(t.Context(), "123", "TXT") + records, err := client.GetRecords(context.Background(), "123", "TXT") require.NoError(t, err) expected := []Record{ @@ -182,22 +152,14 @@ func TestClient_GetRecords(t *testing.T) { } func TestClient_GetRecords_error(t *testing.T) { - client := mockBuilder(). - Route("GET /user/v1/zones/123/records", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client := setupTest(t, "/user/v1/zones/123/records", testHandler(http.MethodGet, http.StatusUnauthorized, "error.json")) - _, err := client.GetRecords(t.Context(), "123", "TXT") - require.EqualError(t, err, "401: Unauthenticated.") + _, err := client.GetRecords(context.Background(), "123", "TXT") + require.Error(t, err) } func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /user/v1/zones/123/records", - servermock.ResponseFromFixture("record.json"). - WithStatusCode(http.StatusCreated)). - Build(t) + client := setupTest(t, "/user/v1/zones/123/records", testHandler(http.MethodPost, http.StatusCreated, "record.json")) record := Record{ Type: "TXT", @@ -207,7 +169,7 @@ func TestClient_AddRecord(t *testing.T) { Comment: "example", } - newRecord, err := client.AddRecord(t.Context(), "123", record) + newRecord, err := client.AddRecord(context.Background(), "123", record) require.NoError(t, err) expected := &Record{ @@ -223,11 +185,7 @@ func TestClient_AddRecord(t *testing.T) { } func TestClient_AddRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /user/v1/zones/123/records", - servermock.ResponseFromFixture("error-details.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client := setupTest(t, "/user/v1/zones/123/records", testHandler(http.MethodPost, http.StatusUnauthorized, "error-details.json")) record := Record{ Type: "TXT", @@ -237,28 +195,69 @@ func TestClient_AddRecord_error(t *testing.T) { Comment: "example", } - _, err := client.AddRecord(t.Context(), "123", record) - require.EqualError(t, err, "401: The given data was invalid. type: [Darf nicht leer sein.]") + _, err := client.AddRecord(context.Background(), "123", record) + require.Error(t, err) } func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /user/v1/zones/123/records/6", - servermock.Noop().WithStatusCode(http.StatusNoContent). - WithStatusCode(http.StatusCreated)). - Build(t) + client := setupTest(t, "/user/v1/zones/123/records/6", testHandler(http.MethodDelete, http.StatusUnauthorized, "error.json")) - err := client.DeleteRecord(t.Context(), "123", "6") + err := client.DeleteRecord(context.Background(), "123", "6") require.Error(t, err) } func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /user/v1/zones/123/records/6", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client := setupTest(t, "/user/v1/zones/123/records/6", testHandler(http.MethodDelete, http.StatusNoContent, "")) - err := client.DeleteRecord(t.Context(), "123", "6") - require.EqualError(t, err, "401: Unauthenticated.") + err := client.DeleteRecord(context.Background(), "123", "6") + require.NoError(t, err) +} + +func setupTest(t *testing.T, path string, handler http.Handler) *Client { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.Handle(path, handler) + + client := NewClient(OAuthStaticAccessToken(server.Client(), testAPIKey)) + client.baseURL, _ = url.Parse(server.URL) + + return client +} + +func testHandler(method string, statusCode int, filename string) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed) + return + } + + if req.Header.Get("Authorization") != "Bearer "+testAPIKey { + http.Error(rw, `{"message":"Unauthenticated"}`, http.StatusUnauthorized) + return + } + + rw.WriteHeader(statusCode) + + if statusCode == http.StatusNoContent { + return + } + + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError) + return + } + + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError) + return + } + } } diff --git a/providers/dns/hosttech/internal/types.go b/providers/dns/hosttech/internal/types.go index a4b5b564d..bf86964f7 100644 --- a/providers/dns/hosttech/internal/types.go +++ b/providers/dns/hosttech/internal/types.go @@ -2,7 +2,6 @@ package internal import ( "fmt" - "strings" ) type apiResponse[T any] struct { @@ -16,15 +15,11 @@ type APIError struct { } func (a APIError) Error() string { - msg := new(strings.Builder) - - _, _ = fmt.Fprintf(msg, "%d: %s", a.StatusCode, a.Message) - + msg := fmt.Sprintf("%d: %s", a.StatusCode, a.Message) for k, v := range a.Errors { - _, _ = fmt.Fprintf(msg, " %s: %v", k, v) + msg += fmt.Sprintf(" %s: %v", k, v) } - - return msg.String() + return msg } type Zone struct { diff --git a/providers/dns/httpnet/httpnet.go b/providers/dns/httpnet/httpnet.go index 4a88f1092..88a13f469 100644 --- a/providers/dns/httpnet/httpnet.go +++ b/providers/dns/httpnet/httpnet.go @@ -2,12 +2,14 @@ package httpnet import ( + "context" "errors" "fmt" "net/http" + "net/url" + "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/internal/hostingde" @@ -26,20 +28,22 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const defaultBaseURL = "https://partner.http.net/api/dns/v1/json" - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. -type Config = hostingde.Config +type Config struct { + APIKey string + ZoneName string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - ZoneName: env.GetOrFile(EnvZoneName), TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, @@ -48,7 +52,11 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - prv challenge.ProviderTimeout + config *Config + client *hostingde.Client + + recordIDs map[string]string + recordIDsMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance configured for http.net. @@ -62,6 +70,7 @@ func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] + config.ZoneName = env.GetOrFile(EnvZoneName) return NewDNSProviderConfig(config) } @@ -72,36 +81,143 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("httpnet: the configuration of the DNS provider is nil") } - provider, err := hostingde.NewDNSProviderConfig(config, defaultBaseURL) - if err != nil { - return nil, fmt.Errorf("httpnet: %w", err) + if config.APIKey == "" { + return nil, errors.New("httpnet: API key missing") } - return &DNSProvider{prv: provider}, nil + client := hostingde.NewClient(config.APIKey) + client.BaseURL, _ = url.Parse(hostingde.DefaultHTTPNetBaseURL) + + return &DNSProvider{ + config: config, + client: client, + recordIDs: make(map[string]string), + }, nil } -// Present creates a TXT record using the specified parameters. +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - err := d.prv.Present(domain, token, keyAuth) + info := dns01.GetChallengeInfo(domain, keyAuth) + + zoneName, err := d.getZoneName(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("httpnet: could not find zone for domain %q: %w", domain, err) + } + + ctx := context.Background() + + // get the ZoneConfig for that domain + zonesFind := hostingde.ZoneConfigsFindRequest{ + Filter: hostingde.Filter{Field: "zoneName", Value: zoneName}, + Limit: 1, + Page: 1, + } + + zoneConfig, err := d.client.GetZone(ctx, zonesFind) if err != nil { return fmt.Errorf("httpnet: %w", err) } + zoneConfig.Name = zoneName + + rec := []hostingde.DNSRecord{{ + Type: "TXT", + Name: dns01.UnFqdn(info.EffectiveFQDN), + Content: info.Value, + TTL: d.config.TTL, + }} + + req := hostingde.ZoneUpdateRequest{ + ZoneConfig: *zoneConfig, + RecordsToAdd: rec, + } + + response, err := d.client.UpdateZone(ctx, req) + if err != nil { + return fmt.Errorf("httpnet: %w", err) + } + + for _, record := range response.Records { + if record.Name == dns01.UnFqdn(info.EffectiveFQDN) && record.Content == fmt.Sprintf(`%q`, info.Value) { + d.recordIDsMu.Lock() + d.recordIDs[info.EffectiveFQDN] = record.ID + d.recordIDsMu.Unlock() + } + } + + if d.recordIDs[info.EffectiveFQDN] == "" { + return fmt.Errorf("httpnet: error getting ID of just created record, for domain %s", domain) + } + return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - err := d.prv.CleanUp(domain, token, keyAuth) + info := dns01.GetChallengeInfo(domain, keyAuth) + + zoneName, err := d.getZoneName(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("httpnet: could not find zone for domain %q: %w", domain, err) + } + + ctx := context.Background() + + // get the ZoneConfig for that domain + zonesFind := hostingde.ZoneConfigsFindRequest{ + Filter: hostingde.Filter{Field: "zoneName", Value: zoneName}, + Limit: 1, + Page: 1, + } + + zoneConfig, err := d.client.GetZone(ctx, zonesFind) if err != nil { return fmt.Errorf("httpnet: %w", err) } + zoneConfig.Name = zoneName + rec := []hostingde.DNSRecord{{ + Type: "TXT", + Name: dns01.UnFqdn(info.EffectiveFQDN), + Content: `"` + info.Value + `"`, + }} + + req := hostingde.ZoneUpdateRequest{ + ZoneConfig: *zoneConfig, + RecordsToDelete: rec, + } + + // Delete record ID from map + d.recordIDsMu.Lock() + delete(d.recordIDs, info.EffectiveFQDN) + d.recordIDsMu.Unlock() + + _, err = d.client.UpdateZone(ctx, req) + if err != nil { + return fmt.Errorf("httpnet: %w", err) + } return nil } -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.prv.Timeout() +func (d *DNSProvider) getZoneName(fqdn string) (string, error) { + if d.config.ZoneName != "" { + return d.config.ZoneName, nil + } + + zoneName, err := dns01.FindZoneByFqdn(fqdn) + if err != nil { + return "", fmt.Errorf("could not find zone: %w", err) + } + + if zoneName == "" { + return "", errors.New("empty zone name") + } + + return dns01.UnFqdn(zoneName), nil } diff --git a/providers/dns/httpnet/httpnet.toml b/providers/dns/httpnet/httpnet.toml index 3dd581204..a465d06e9 100644 --- a/providers/dns/httpnet/httpnet.toml +++ b/providers/dns/httpnet/httpnet.toml @@ -6,7 +6,7 @@ Since = "v4.15.0" Example = ''' HTTPNET_API_KEY=xxxxxxxx \ -lego --dns httpnet -d '*.example.com' -d example.com run +lego --email you@example.com --dns httpnet --domains my.example.org run ''' [Configuration] @@ -14,10 +14,10 @@ lego --dns httpnet -d '*.example.com' -d example.com run HTTPNET_API_KEY = "API key" [Configuration.Additional] HTTPNET_ZONE_NAME = "Zone name in ACE format" - HTTPNET_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - HTTPNET_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - HTTPNET_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - HTTPNET_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + HTTPNET_POLLING_INTERVAL = "Time between DNS propagation check" + HTTPNET_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + HTTPNET_TTL = "The TTL of the TXT record used for the DNS challenge" + HTTPNET_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.http.net/docs/api/#dns" diff --git a/providers/dns/httpnet/httpnet_test.go b/providers/dns/httpnet/httpnet_test.go index ef1d2a1b7..a9bc527ad 100644 --- a/providers/dns/httpnet/httpnet_test.go +++ b/providers/dns/httpnet/httpnet_test.go @@ -49,7 +49,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -59,7 +58,8 @@ func TestNewDNSProvider(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - require.NotNil(t, p.prv) + require.NotNil(t, p.config) + require.NotNil(t, p.recordIDs) } else { require.EqualError(t, err, test.expected) } @@ -101,7 +101,8 @@ func TestNewDNSProviderConfig(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - require.NotNil(t, p.prv) + require.NotNil(t, p.config) + require.NotNil(t, p.recordIDs) } else { require.EqualError(t, err, test.expected) } @@ -115,7 +116,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -129,7 +129,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/httpreq/httpreq.go b/providers/dns/httpreq/httpreq.go index 591e9b5e1..81b3a6982 100644 --- a/providers/dns/httpreq/httpreq.go +++ b/providers/dns/httpreq/httpreq.go @@ -11,10 +11,8 @@ import ( "net/url" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/internal/errutils" ) @@ -32,8 +30,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - type message struct { FQDN string `json:"fqdn"` Value string `json:"value"` @@ -89,7 +85,6 @@ func NewDNSProvider() (*DNSProvider, error) { config.Username = env.GetOrFile(EnvUsername) config.Password = env.GetOrFile(EnvPassword) config.Endpoint = endpoint - return NewDNSProviderConfig(config) } @@ -103,8 +98,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("httpreq: the endpoint is missing") } - config.HTTPClient = clientdebug.Wrap(config.HTTPClient) - return &DNSProvider{config: config}, nil } @@ -129,7 +122,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("httpreq: %w", err) } - return nil } @@ -143,7 +135,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("httpreq: %w", err) } - return nil } @@ -162,7 +153,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("httpreq: %w", err) } - return nil } @@ -176,13 +166,11 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("httpreq: %w", err) } - return nil } func (d *DNSProvider) doPost(ctx context.Context, uri string, msg any) error { reqBody := new(bytes.Buffer) - err := json.NewEncoder(reqBody).Encode(msg) if err != nil { return fmt.Errorf("failed to create request JSON body: %w", err) diff --git a/providers/dns/httpreq/httpreq.toml b/providers/dns/httpreq/httpreq.toml index d64d61a6c..cd6c823d3 100644 --- a/providers/dns/httpreq/httpreq.toml +++ b/providers/dns/httpreq/httpreq.toml @@ -6,7 +6,7 @@ Since = "v2.0.0" Example = ''' HTTPREQ_ENDPOINT=http://my.server.com:9090 \ -lego --dns httpreq -d '*.example.com' -d example.com run +lego --email you@example.com --dns httpreq --domains my.example.org run ''' Additional = ''' @@ -56,6 +56,6 @@ Basic authentication (optional) can be set with some environment variables: [Configuration.Additional] HTTPREQ_USERNAME = "Basic authentication username" HTTPREQ_PASSWORD = "Basic authentication password" - HTTPREQ_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - HTTPREQ_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - HTTPREQ_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + HTTPREQ_POLLING_INTERVAL = "Time between DNS propagation check" + HTTPREQ_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + HTTPREQ_HTTP_TIMEOUT = "API request timeout" diff --git a/providers/dns/httpreq/httpreq_test.go b/providers/dns/httpreq/httpreq_test.go index 108d6a565..8dc36ccc6 100644 --- a/providers/dns/httpreq/httpreq_test.go +++ b/providers/dns/httpreq/httpreq_test.go @@ -1,12 +1,15 @@ package httpreq import ( + "encoding/json" + "fmt" + "net/http" "net/http/httptest" "net/url" + "path" "testing" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/require" ) @@ -43,7 +46,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -100,60 +102,75 @@ func TestNewDNSProvider_Present(t *testing.T) { testCases := []struct { desc string - builder *servermock.Builder[*DNSProvider] + mode string + username string + password string + pathPrefix string + handler http.HandlerFunc expectedError string }{ { - desc: "success", - builder: mockBuilder(""). - Route("/present", - servermock.RawStringResponse("lego"), - servermock.CheckRequestJSONBody(`{"fqdn":"_acme-challenge.domain.","value":"LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM"}`)), + desc: "success", + handler: successHandler, }, { - desc: "success with path prefix", - builder: mockBuilderWithPathPrefix("", "/api/acme/"). - Route("/api/acme/present", - servermock.RawStringResponse("lego"), - servermock.CheckRequestJSONBody(`{"fqdn":"_acme-challenge.domain.","value":"LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM"}`)), + desc: "success with path prefix", + handler: successHandler, + pathPrefix: "/api/acme/", }, { desc: "error", - builder: mockBuilder(""), + handler: http.NotFound, expectedError: "httpreq: unexpected status code: [status code: 404] body: 404 page not found", }, { - desc: "success raw mode", - builder: mockBuilder("RAW"). - Route("/present", - servermock.RawStringResponse("lego"), - servermock.CheckRequestBody(`{"domain":"domain","token":"token","keyAuth":"key"}`)), + desc: "success raw mode", + mode: "RAW", + handler: successRawModeHandler, }, { desc: "error raw mode", - builder: mockBuilder("RAW"), + mode: "RAW", + handler: http.NotFound, expectedError: "httpreq: unexpected status code: [status code: 404] body: 404 page not found", }, { - desc: "basic auth fail", - builder: mockBuilderWithBasicAuth("nope", "nope"). - Route("/present", servermock.Noop()), - expectedError: `httpreq: unexpected status code: [status code: 400] body: invalid credentials: got [username: "nope", password: "nope"], want [username: "user", password: "secret"]`, - }, - { - desc: "basic auth success", - builder: mockBuilderWithBasicAuth("user", "secret"). - Route("/present", - servermock.RawStringResponse("lego"), - servermock.CheckRequestJSONBody(`{"fqdn":"_acme-challenge.domain.","value":"LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM"}`)), + desc: "basic auth", + username: "bar", + password: "foo", + handler: func(rw http.ResponseWriter, req *http.Request) { + username, password, ok := req.BasicAuth() + if username != "bar" || password != "foo" || !ok { + rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q`, "Please enter your username and password.")) + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + fmt.Fprint(rw, "lego") + }, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - p := test.builder.Build(t) + t.Parallel() - err := p.Present("domain", "token", "key") + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc(path.Join("/", test.pathPrefix, "present"), test.handler) + + config := NewDefaultConfig() + config.Endpoint = mustParse(server.URL + test.pathPrefix) + config.Mode = test.mode + config.Username = test.username + config.Password = test.password + + p, err := NewDNSProviderConfig(config) + require.NoError(t, err) + + err = p.Present("domain", "token", "key") if test.expectedError == "" { require.NoError(t, err) } else { @@ -168,53 +185,68 @@ func TestNewDNSProvider_Cleanup(t *testing.T) { testCases := []struct { desc string - builder *servermock.Builder[*DNSProvider] + mode string + username string + password string + handler http.HandlerFunc expectedError string }{ { - desc: "success", - builder: mockBuilder(""). - Route("/cleanup", - servermock.RawStringResponse("lego"), - servermock.CheckRequestJSONBody(`{"fqdn":"_acme-challenge.domain.","value":"LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM"}`)), + desc: "success", + handler: successHandler, }, { desc: "error", - builder: mockBuilder(""), + handler: http.NotFound, expectedError: "httpreq: unexpected status code: [status code: 404] body: 404 page not found", }, { - desc: "success raw mode", - builder: mockBuilder("RAW"). - Route("/cleanup", - servermock.RawStringResponse("lego"), - servermock.CheckRequestBody(`{"domain":"domain","token":"token","keyAuth":"key"}`)), + desc: "success raw mode", + mode: "RAW", + handler: successRawModeHandler, }, { desc: "error raw mode", - builder: mockBuilder("RAW"), + mode: "RAW", + handler: http.NotFound, expectedError: "httpreq: unexpected status code: [status code: 404] body: 404 page not found", }, { - desc: "basic auth fail", - builder: mockBuilderWithBasicAuth("test", "example"). - Route("/cleanup", servermock.Noop()), - expectedError: `httpreq: unexpected status code: [status code: 400] body: invalid credentials: got [username: "test", password: "example"], want [username: "user", password: "secret"]`, - }, - { - desc: "basic auth success", - builder: mockBuilderWithBasicAuth("user", "secret"). - Route("/cleanup", - servermock.RawStringResponse("lego"), - servermock.CheckRequestJSONBody(`{"fqdn":"_acme-challenge.domain.","value":"LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM"}`)), + desc: "basic auth", + username: "bar", + password: "foo", + handler: func(rw http.ResponseWriter, req *http.Request) { + username, password, ok := req.BasicAuth() + if username != "bar" || password != "foo" || !ok { + rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q`, "Please enter your username and password.")) + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + fmt.Fprint(rw, "lego") + }, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - p := test.builder.Build(t) + t.Parallel() - err := p.CleanUp("domain", "token", "key") + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc("/cleanup", test.handler) + + config := NewDefaultConfig() + config.Endpoint = mustParse(server.URL) + config.Mode = test.mode + config.Username = test.username + config.Password = test.password + + p, err := NewDNSProviderConfig(config) + require.NoError(t, err) + + err = p.CleanUp("domain", "token", "key") if test.expectedError == "" { require.NoError(t, err) } else { @@ -224,42 +256,36 @@ func TestNewDNSProvider_Cleanup(t *testing.T) { } } -func mockBuilder(mode string) *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.HTTPClient = server.Client() - config.Endpoint, _ = url.Parse(server.URL) - config.Mode = mode +func successHandler(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } - return NewDNSProviderConfig(config) - }) + msg := &message{} + err := json.NewDecoder(req.Body).Decode(msg) + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + fmt.Fprint(rw, "lego") } -func mockBuilderWithPathPrefix(mode, prefix string) *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.HTTPClient = server.Client() - config.Endpoint, _ = url.Parse(server.URL + prefix) - config.Mode = mode +func successRawModeHandler(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } - return NewDNSProviderConfig(config) - }) -} + msg := &messageRaw{} + err := json.NewDecoder(req.Body).Decode(msg) + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } -func mockBuilderWithBasicAuth(username, password string) *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.HTTPClient = server.Client() - config.Endpoint, _ = url.Parse(server.URL) - config.Username = username - config.Password = password - - return NewDNSProviderConfig(config) - }, - servermock.CheckHeader().WithBasicAuth("user", "secret")) + fmt.Fprint(rw, "lego") } func mustParse(rawURL string) *url.URL { @@ -267,6 +293,5 @@ func mustParse(rawURL string) *url.URL { if err != nil { panic(err) } - return uri } diff --git a/providers/dns/huaweicloud/huaweicloud.go b/providers/dns/huaweicloud/huaweicloud.go deleted file mode 100644 index e47f3e2b5..000000000 --- a/providers/dns/huaweicloud/huaweicloud.go +++ /dev/null @@ -1,297 +0,0 @@ -// Package huaweicloud implements a DNS provider for solving the DNS-01 challenge using Huawei Cloud. -package huaweicloud - -import ( - "context" - "errors" - "fmt" - "strconv" - "strings" - "sync" - "time" - - "github.com/cenkalti/backoff/v5" - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/platform/wait" - "github.com/go-acme/lego/v4/providers/dns/huaweicloud/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/ptr" - hwauthbasic "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic" - hwconfig "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/config" - hwdns "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2" - hwmodel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/model" - hwregion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/region" -) - -// Environment variables names. -const ( - envNamespace = "HUAWEICLOUD_" - - EnvAccessKeyID = envNamespace + "ACCESS_KEY_ID" - EnvSecretAccessKey = envNamespace + "SECRET_ACCESS_KEY" - EnvRegion = envNamespace + "REGION" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - AccessKeyID string - SecretAccessKey string - Region string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int32 - HTTPTimeout time.Duration -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: int32(env.GetOrDefaultInt(EnvTTL, 300)), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.DnsClient - - recordIDs map[string]string - recordIDsMu sync.Mutex -} - -// NewDNSProvider returns a DNSProvider instance configured for Huawei Cloud. -// Credentials must be passed in the environment variables: -// HUAWEICLOUD_ACCESS_KEY_ID, HUAWEICLOUD_SECRET_ACCESS_KEY, and HUAWEICLOUD_REGION. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAccessKeyID, EnvSecretAccessKey, EnvRegion) - if err != nil { - return nil, fmt.Errorf("huaweicloud: %w", err) - } - - config := NewDefaultConfig() - config.AccessKeyID = values[EnvAccessKeyID] - config.SecretAccessKey = values[EnvSecretAccessKey] - config.Region = values[EnvRegion] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Huawei Cloud. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("huaweicloud: the configuration of the DNS provider is nil") - } - - if config.AccessKeyID == "" || config.SecretAccessKey == "" || config.Region == "" { - return nil, errors.New("huaweicloud: credentials missing") - } - - auth, err := hwauthbasic.NewCredentialsBuilder(). - WithAk(config.AccessKeyID). - WithSk(config.SecretAccessKey). - SafeBuild() - if err != nil { - return nil, fmt.Errorf("huaweicloud: crendential build: %w", err) - } - - region, err := hwregion.SafeValueOf(config.Region) - if err != nil { - return nil, fmt.Errorf("huaweicloud: safe region: %w", err) - } - - client, err := hwdns.DnsClientBuilder(). - WithHttpConfig(hwconfig.DefaultHttpConfig().WithTimeout(config.HTTPTimeout)). - WithRegion(region). - WithCredential(auth). - SafeBuild() - if err != nil { - return nil, fmt.Errorf("huaweicloud: client build: %w", err) - } - - return &DNSProvider{ - config: config, - client: internal.NewDnsClient(client), - recordIDs: map[string]string{}, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("huaweicloud: could not find zone for domain %q: %w", domain, err) - } - - zoneID, err := d.getZoneID(authZone) - if err != nil { - return fmt.Errorf("huaweicloud: %w", err) - } - - recordSetID, err := d.getOrCreateRecordSetID(domain, zoneID, info) - if err != nil { - return fmt.Errorf("huaweicloud: %w", err) - } - - d.recordIDsMu.Lock() - d.recordIDs[token] = recordSetID - d.recordIDsMu.Unlock() - - err = wait.Retry(context.Background(), - func() error { - rs, errShow := d.client.ShowRecordSet(&hwmodel.ShowRecordSetRequest{ - ZoneId: zoneID, - RecordsetId: recordSetID, - }) - if errShow != nil { - return fmt.Errorf("show record set: %w", errShow) - } - - if !strings.HasSuffix(ptr.Deref(rs.Status), "PENDING_") { - return nil - } - - return fmt.Errorf("status: %s", ptr.Deref(rs.Status)) - }, - backoff.WithBackOff(backoff.NewConstantBackOff(d.config.PollingInterval)), - backoff.WithMaxElapsedTime(d.config.PropagationTimeout), - ) - if err != nil { - return fmt.Errorf("huaweicloud: record set sync on %s: %w", domain, err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - // gets the record's unique ID from when we created it - d.recordIDsMu.Lock() - recordID, ok := d.recordIDs[token] - d.recordIDsMu.Unlock() - - if !ok { - return fmt.Errorf("huaweicloud: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) - } - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("huaweicloud: could not find zone for domain %q: %w", domain, err) - } - - zoneID, err := d.getZoneID(authZone) - if err != nil { - return fmt.Errorf("huaweicloud: %w", err) - } - - request := &hwmodel.DeleteRecordSetRequest{ - ZoneId: zoneID, - RecordsetId: recordID, - } - - _, err = d.client.DeleteRecordSet(request) - if err != nil { - return fmt.Errorf("huaweicloud: delete record: %w", err) - } - - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -func (d *DNSProvider) getOrCreateRecordSetID(domain, zoneID string, info dns01.ChallengeInfo) (string, error) { - records, err := d.client.ListRecordSetsByZone(&hwmodel.ListRecordSetsByZoneRequest{ - ZoneId: zoneID, - Name: ptr.Pointer(info.EffectiveFQDN), - }) - if err != nil { - return "", fmt.Errorf("record list: unable to get record %s for zone %s: %w", info.EffectiveFQDN, domain, err) - } - - var existingRecordSet *hwmodel.ListRecordSets - - for _, record := range ptr.Deref(records.Recordsets) { - if ptr.Deref(record.Type) == "TXT" && ptr.Deref(record.Name) == info.EffectiveFQDN { - existingRecordSet = &record - } - } - - value := strconv.Quote(info.Value) - - if existingRecordSet == nil { - request := &hwmodel.CreateRecordSetRequest{ - ZoneId: zoneID, - Body: &hwmodel.CreateRecordSetRequestBody{ - Name: info.EffectiveFQDN, - Description: ptr.Pointer("Added TXT record for ACME dns-01 challenge using lego client"), - Type: "TXT", - Ttl: ptr.Pointer(d.config.TTL), - Records: []string{value}, - }, - } - - resp, errCreate := d.client.CreateRecordSet(request) - if errCreate != nil { - return "", fmt.Errorf("create record set: %w", errCreate) - } - - return ptr.Deref(resp.Id), nil - } - - updateRequest := &hwmodel.UpdateRecordSetRequest{ - ZoneId: zoneID, - RecordsetId: ptr.Deref(existingRecordSet.Id), - Body: &hwmodel.UpdateRecordSetReq{ - Name: existingRecordSet.Name, - Description: existingRecordSet.Description, - Type: existingRecordSet.Type, - Ttl: existingRecordSet.Ttl, - Records: ptr.Pointer(append(ptr.Deref(existingRecordSet.Records), value)), - }, - } - - resp, err := d.client.UpdateRecordSet(updateRequest) - if err != nil { - return "", fmt.Errorf("update record set: %w", err) - } - - return ptr.Deref(resp.Id), nil -} - -func (d *DNSProvider) getZoneID(authZone string) (string, error) { - zones, err := d.client.ListPublicZones(&hwmodel.ListPublicZonesRequest{}) - if err != nil { - return "", fmt.Errorf("unable to get zone: %w", err) - } - - for _, zone := range ptr.Deref(zones.Zones) { - if ptr.Deref(zone.Name) == authZone { - return ptr.Deref(zone.Id), nil - } - } - - return "", fmt.Errorf("zone %q not found", authZone) -} diff --git a/providers/dns/huaweicloud/huaweicloud.toml b/providers/dns/huaweicloud/huaweicloud.toml deleted file mode 100644 index e8d417c11..000000000 --- a/providers/dns/huaweicloud/huaweicloud.toml +++ /dev/null @@ -1,29 +0,0 @@ -Name = "Huawei Cloud" -Description = '''''' -URL = "https://huaweicloud.com" -Code = "huaweicloud" -Since = "v4.19" - -Example = ''' -HUAWEICLOUD_ACCESS_KEY_ID=your-access-key-id \ -HUAWEICLOUD_SECRET_ACCESS_KEY=your-secret-access-key \ -HUAWEICLOUD_REGION=cn-south-1 \ -lego --dns huaweicloud -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - HUAWEICLOUD_ACCESS_KEY_ID = "Access key ID" - HUAWEICLOUD_SECRET_ACCESS_KEY = "Access Key secret" - HUAWEICLOUD_REGION = "Region" - - [Configuration.Additional] - HUAWEICLOUD_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - HUAWEICLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - HUAWEICLOUD_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - HUAWEICLOUD_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://console-intl.huaweicloud.com/apiexplorer/#/openapi/DNS/doc?locale=en-us" - CN_API = "https://support.huaweicloud.com/api-dns/zh-cn_topic_0132421999.html" - GoClient = "https://github.com/huaweicloud/huaweicloud-sdk-go-v3" diff --git a/providers/dns/huaweicloud/huaweicloud_test.go b/providers/dns/huaweicloud/huaweicloud_test.go deleted file mode 100644 index 25e295da7..000000000 --- a/providers/dns/huaweicloud/huaweicloud_test.go +++ /dev/null @@ -1,166 +0,0 @@ -package huaweicloud - -import ( - "testing" - "time" - - "github.com/go-acme/lego/v4/platform/tester" - hwregion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/region" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAccessKeyID, EnvSecretAccessKey, EnvRegion). - WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - // The "success" cannot be tested because there is an API call that require a valid authentication. - { - desc: "missing credentials", - envVars: map[string]string{ - EnvAccessKeyID: "", - EnvSecretAccessKey: "", - EnvRegion: "", - }, - expected: "huaweicloud: some credentials information are missing: HUAWEICLOUD_ACCESS_KEY_ID,HUAWEICLOUD_SECRET_ACCESS_KEY,HUAWEICLOUD_REGION", - }, - { - desc: "missing access id", - envVars: map[string]string{ - EnvAccessKeyID: "", - EnvSecretAccessKey: "456", - EnvRegion: hwregion.CN_EAST_2.Id, - }, - expected: "huaweicloud: some credentials information are missing: HUAWEICLOUD_ACCESS_KEY_ID", - }, - { - desc: "missing secret key", - envVars: map[string]string{ - EnvAccessKeyID: "123", - EnvSecretAccessKey: "", - EnvRegion: hwregion.CN_EAST_2.Id, - }, - expected: "huaweicloud: some credentials information are missing: HUAWEICLOUD_SECRET_ACCESS_KEY", - }, - { - desc: "missing secret key", - envVars: map[string]string{ - EnvAccessKeyID: "123", - EnvSecretAccessKey: "456", - EnvRegion: "", - }, - expected: "huaweicloud: some credentials information are missing: HUAWEICLOUD_REGION", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - accessKeyID string - secretAccessKey string - region string - expected string - }{ - // The "success" cannot be tested because there is an API call that require a valid authentication. - { - desc: "missing credentials", - expected: "huaweicloud: credentials missing", - }, - { - desc: "missing secret id", - secretAccessKey: "456", - region: hwregion.CN_EAST_2.Id, - expected: "huaweicloud: credentials missing", - }, - { - desc: "missing secret key", - accessKeyID: "123", - region: hwregion.CN_EAST_2.Id, - expected: "huaweicloud: credentials missing", - }, - { - desc: "missing region", - accessKeyID: "123", - secretAccessKey: "456", - expected: "huaweicloud: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.AccessKeyID = test.accessKeyID - config.SecretAccessKey = test.secretAccessKey - config.Region = test.region - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - time.Sleep(1 * time.Second) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/huaweicloud/internal/client.go b/providers/dns/huaweicloud/internal/client.go deleted file mode 100644 index f10cf2dff..000000000 --- a/providers/dns/huaweicloud/internal/client.go +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright (c) Huawei Technologies Co., Ltd. 2020-present. All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Package internal is a partial copy of https://github.com/huaweicloud/huaweicloud-sdk-go-v3/blob/v0.1.159/services/dns/v2/dns_client.go -package internal - -import ( - httpclient "github.com/huaweicloud/huaweicloud-sdk-go-v3/core" - hwdns "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2" - "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/model" -) - -type DnsClient struct { - HcClient *httpclient.HcHttpClient -} - -func NewDnsClient(hcClient *httpclient.HcHttpClient) *DnsClient { - return &DnsClient{HcClient: hcClient} -} - -func (c *DnsClient) ShowRecordSet(request *model.ShowRecordSetRequest) (*model.ShowRecordSetResponse, error) { - requestDef := hwdns.GenReqDefForShowRecordSet() - - if resp, err := c.HcClient.Sync(request, requestDef); err != nil { - return nil, err - } else { - return resp.(*model.ShowRecordSetResponse), nil - } -} - -func (c *DnsClient) CreateRecordSet(request *model.CreateRecordSetRequest) (*model.CreateRecordSetResponse, error) { - requestDef := hwdns.GenReqDefForCreateRecordSet() - - if resp, err := c.HcClient.Sync(request, requestDef); err != nil { - return nil, err - } else { - return resp.(*model.CreateRecordSetResponse), nil - } -} - -func (c *DnsClient) UpdateRecordSet(request *model.UpdateRecordSetRequest) (*model.UpdateRecordSetResponse, error) { - requestDef := hwdns.GenReqDefForUpdateRecordSet() - - if resp, err := c.HcClient.Sync(request, requestDef); err != nil { - return nil, err - } else { - return resp.(*model.UpdateRecordSetResponse), nil - } -} - -func (c *DnsClient) DeleteRecordSet(request *model.DeleteRecordSetRequest) (*model.DeleteRecordSetResponse, error) { - requestDef := hwdns.GenReqDefForDeleteRecordSet() - - if resp, err := c.HcClient.Sync(request, requestDef); err != nil { - return nil, err - } else { - return resp.(*model.DeleteRecordSetResponse), nil - } -} - -func (c *DnsClient) ListRecordSetsByZone(request *model.ListRecordSetsByZoneRequest) (*model.ListRecordSetsByZoneResponse, error) { - requestDef := hwdns.GenReqDefForListRecordSetsByZone() - - if resp, err := c.HcClient.Sync(request, requestDef); err != nil { - return nil, err - } else { - return resp.(*model.ListRecordSetsByZoneResponse), nil - } -} - -func (c *DnsClient) ListPublicZones(request *model.ListPublicZonesRequest) (*model.ListPublicZonesResponse, error) { - requestDef := hwdns.GenReqDefForListPublicZones() - - if resp, err := c.HcClient.Sync(request, requestDef); err != nil { - return nil, err - } else { - return resp.(*model.ListPublicZonesResponse), nil - } -} diff --git a/providers/dns/hurricane/hurricane.go b/providers/dns/hurricane/hurricane.go index b23528bb0..d17ceb892 100644 --- a/providers/dns/hurricane/hurricane.go +++ b/providers/dns/hurricane/hurricane.go @@ -5,13 +5,12 @@ import ( "errors" "fmt" "net/http" + "strings" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/hurricane/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -26,8 +25,6 @@ const ( EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Credentials map[string]string @@ -58,15 +55,14 @@ type DNSProvider struct { // NewDNSProvider returns a DNSProvider instance configured for Hurricane Electric. func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() - values, err := env.Get(EnvTokens) if err != nil { return nil, fmt.Errorf("hurricane: %w", err) } - credentials, err := env.ParsePairs(values[EnvTokens]) + credentials, err := parseCredentials(values[EnvTokens]) if err != nil { - return nil, fmt.Errorf("hurricane: credentials: %w", err) + return nil, fmt.Errorf("hurricane: %w", err) } config.Credentials = credentials @@ -85,12 +81,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client := internal.NewClient(config.Credentials) - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{config: config, client: client}, nil } @@ -129,3 +119,19 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { func (d *DNSProvider) Sequential() time.Duration { return d.config.SequenceInterval } + +func parseCredentials(raw string) (map[string]string, error) { + credentials := make(map[string]string) + + credStrings := strings.Split(strings.TrimSuffix(raw, ","), ",") + for _, credPair := range credStrings { + data := strings.Split(credPair, ":") + if len(data) != 2 { + return nil, fmt.Errorf("incorrect credential pair: %s", credPair) + } + + credentials[strings.TrimSpace(data[0])] = strings.TrimSpace(data[1]) + } + + return credentials, nil +} diff --git a/providers/dns/hurricane/hurricane.toml b/providers/dns/hurricane/hurricane.toml index 10b370e4f..a8d36c43d 100644 --- a/providers/dns/hurricane/hurricane.toml +++ b/providers/dns/hurricane/hurricane.toml @@ -6,10 +6,10 @@ Since = "v4.3.0" Example = ''' HURRICANE_TOKENS=example.org:token \ -lego --dns hurricane -d '*.example.com' -d example.com run +lego --email you@example.com --dns hurricane --domains example.org --domains '*.example.org' run HURRICANE_TOKENS=my.example.org:token1,demo.example.org:token2 \ -lego --dns hurricane -d my.example.org -d demo.example.org +lego --email you@example.com --dns hurricane --domains my.example.org --domains demo.example.org ''' Additional = """ @@ -38,11 +38,11 @@ HURRICANE_TOKENS=example.org:token [Configuration] [Configuration.Credentials] HURRICANE_TOKENS = "TXT record names and tokens" - [Configuration.Additional] - HURRICANE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - HURRICANE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation (Default: 300)" - HURRICANE_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 60)" - HURRICANE_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + [Configuration.Addtional] + HURRICANE_POLLING_INTERVAL = "Time between DNS propagation checks" + HURRICANE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation; defaults to 300s (5 minutes)" + HURRICANE_SEQUENCE_INTERVAL = "Time between sequential requests" + HURRICANE_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://dns.he.net/" diff --git a/providers/dns/hurricane/hurricane_test.go b/providers/dns/hurricane/hurricane_test.go index 2bbd638fa..12217c790 100644 --- a/providers/dns/hurricane/hurricane_test.go +++ b/providers/dns/hurricane/hurricane_test.go @@ -34,14 +34,14 @@ func TestNewDNSProvider(t *testing.T) { envVars: map[string]string{ EnvTokens: ",", }, - expected: "hurricane: credentials: incorrect pair: ", + expected: "hurricane: incorrect credential pair: ", }, { desc: "invalid credentials, partial", envVars: map[string]string{ EnvTokens: "example.org:123,example.net", }, - expected: "hurricane: credentials: incorrect pair: example.net", + expected: "hurricane: incorrect credential pair: example.net", }, { desc: "missing credentials", @@ -55,7 +55,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -121,7 +120,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -135,7 +133,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/hurricane/internal/client.go b/providers/dns/hurricane/internal/client.go index b758ec166..62ca76159 100644 --- a/providers/dns/hurricane/internal/client.go +++ b/providers/dns/hurricane/internal/client.go @@ -52,7 +52,7 @@ func NewClient(credentials map[string]string) *Client { } // UpdateTxtRecord updates a TXT record. -func (c *Client) UpdateTxtRecord(ctx context.Context, hostname, txt string) error { +func (c *Client) UpdateTxtRecord(ctx context.Context, hostname string, txt string) error { domain := strings.TrimPrefix(hostname, "_acme-challenge.") c.credMu.Lock() @@ -101,7 +101,7 @@ func (c *Client) UpdateTxtRecord(ctx context.Context, hostname, txt string) erro return evaluateBody(string(bytes.TrimSpace(raw)), hostname) } -func evaluateBody(body, hostname string) error { +func evaluateBody(body string, hostname string) error { code, _, _ := strings.Cut(body, " ") switch code { diff --git a/providers/dns/hurricane/internal/client_test.go b/providers/dns/hurricane/internal/client_test.go index d93f3e0ed..2862c2481 100644 --- a/providers/dns/hurricane/internal/client_test.go +++ b/providers/dns/hurricane/internal/client_test.go @@ -1,21 +1,15 @@ package internal import ( + "context" + "fmt" + "net/http" "net/http/httptest" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" ) -func setupClient(server *httptest.Server) (*Client, error) { - client := NewClient(map[string]string{"example.com": "secret"}) - client.baseURL = server.URL - client.HTTPClient = server.Client() - - return client, nil -} - func TestClient_UpdateTxtRecord(t *testing.T) { testCases := []struct { code string @@ -55,16 +49,33 @@ func TestClient_UpdateTxtRecord(t *testing.T) { t.Run(test.code, func(t *testing.T) { t.Parallel() - client := servermock.NewBuilder[*Client](setupClient, servermock.CheckHeader().WithContentTypeFromURLEncoded()). - Route("POST /", - servermock.RawStringResponse(test.code), - servermock.CheckForm().Strict(). - With("hostname", "_acme-challenge.example.com"). - With("password", "secret"). - With("txt", "foo")). - Build(t) + handler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } - err := client.UpdateTxtRecord(t.Context(), "_acme-challenge.example.com", "foo") + if err := req.ParseForm(); err != nil { + http.Error(rw, "failed to parse form data", http.StatusBadRequest) + return + } + + if req.PostForm.Encode() != "hostname=_acme-challenge.example.com&password=secret&txt=foo" { + http.Error(rw, "invalid form data", http.StatusBadRequest) + return + } + + _, _ = rw.Write([]byte(test.code)) + }) + + server := httptest.NewServer(handler) + t.Cleanup(server.Close) + + client := NewClient(map[string]string{"example.com": "secret"}) + client.baseURL = server.URL + client.HTTPClient = server.Client() + + err := client.UpdateTxtRecord(context.Background(), "_acme-challenge.example.com", "foo") test.expected(t, err) }) } diff --git a/providers/dns/hyperone/hyperone.go b/providers/dns/hyperone/hyperone.go index 3cdad8e68..8578b5c52 100644 --- a/providers/dns/hyperone/hyperone.go +++ b/providers/dns/hyperone/hyperone.go @@ -9,11 +9,9 @@ import ( "path/filepath" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/hyperone/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -30,8 +28,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIEndpoint string @@ -77,7 +73,6 @@ func NewDNSProvider() (*DNSProvider, error) { func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config.PassportLocation == "" { var err error - config.PassportLocation, err = GetDefaultPassportLocation() if err != nil { return nil, fmt.Errorf("hyperone: %w", err) @@ -98,8 +93,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{client: client, config: config}, nil } @@ -167,7 +160,6 @@ func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { if err != nil { return fmt.Errorf("hyperone: %w", err) } - if len(records) == 1 { if records[0].Content != info.Value { return fmt.Errorf("hyperone: record with content %s not found: fqdn=%s", info.Value, info.EffectiveFQDN) diff --git a/providers/dns/hyperone/hyperone.toml b/providers/dns/hyperone/hyperone.toml index 88814356f..bf6d874ef 100644 --- a/providers/dns/hyperone/hyperone.toml +++ b/providers/dns/hyperone/hyperone.toml @@ -5,7 +5,7 @@ Code = "hyperone" Since = "v3.9.0" Example = ''' -lego --dns hyperone -d '*.example.com' -d example.com run +lego --email you@example.com --dns hyperone --domains my.example.org run ''' Additional = ''' @@ -41,10 +41,9 @@ All required permissions are available via platform role `tool.lego`. HYPERONE_PASSPORT_LOCATION = "Allows to pass custom passport file location (default ~/.h1/passport.json)" HYPERONE_API_URL = "Allows to pass custom API Endpoint to be used in the challenge (default https://api.hyperone.com/v2)" HYPERONE_LOCATION_ID = "Specifies location (region) to be used in API calls. (default pl-waw-1)" - HYPERONE_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - HYPERONE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 2)" - HYPERONE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 60)" - HYPERONE_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + HYPERONE_TTL = "The TTL of the TXT record used for the DNS challenge" + HYPERONE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + HYPERONE_POLLING_INTERVAL = "Time between DNS propagation check" [Links] API = "https://api.hyperone.com/v2/docs" diff --git a/providers/dns/hyperone/hyperone_test.go b/providers/dns/hyperone/hyperone_test.go index 675a1fe19..1222d1c74 100644 --- a/providers/dns/hyperone/hyperone_test.go +++ b/providers/dns/hyperone/hyperone_test.go @@ -49,7 +49,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -125,7 +124,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -139,7 +137,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/hyperone/internal/client.go b/providers/dns/hyperone/internal/client.go index cf9ab2a37..09fa68768 100644 --- a/providers/dns/hyperone/internal/client.go +++ b/providers/dns/hyperone/internal/client.go @@ -132,7 +132,7 @@ func (c *Client) CreateRecordset(ctx context.Context, zoneID, recordType, name, // DeleteRecordset deletes a recordset. // https://api.hyperone.com/v2/docs#operation/dns_project_zone_recordset_delete -func (c *Client) DeleteRecordset(ctx context.Context, zoneID, recordsetID string) error { +func (c *Client) DeleteRecordset(ctx context.Context, zoneID string, recordsetID string) error { // https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone/{zoneId}/recordset/{recordsetId} endpoint := c.baseURL.JoinPath("zone", zoneID, "recordset", recordsetID) @@ -146,7 +146,7 @@ func (c *Client) DeleteRecordset(ctx context.Context, zoneID, recordsetID string // GetRecords gets all records within specified recordset. // https://api.hyperone.com/v2/docs#operation/dns_project_zone_recordset_record_list -func (c *Client) GetRecords(ctx context.Context, zoneID, recordsetID string) ([]Record, error) { +func (c *Client) GetRecords(ctx context.Context, zoneID string, recordsetID string) ([]Record, error) { // https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone/{zoneId}/recordset/{recordsetId}/record endpoint := c.baseURL.JoinPath("zone", zoneID, "recordset", recordsetID, "record") diff --git a/providers/dns/hyperone/internal/client_test.go b/providers/dns/hyperone/internal/client_test.go index aa087c4f2..e3a1073e0 100644 --- a/providers/dns/hyperone/internal/client_test.go +++ b/providers/dns/hyperone/internal/client_test.go @@ -1,10 +1,17 @@ package internal import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" "net/http/httptest" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -15,34 +22,10 @@ func (s signerMock) GetJWT() (string, error) { return "", nil } -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - passport := &Passport{ - SubjectID: "/iam/project/proj123/sa/xxxxxxx", - } - - client, err := NewClient(server.URL, "loc123", passport) - if err != nil { - return nil, err - } - - client.HTTPClient = server.Client() - client.signer = signerMock{} - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("Bearer")) -} - func TestClient_FindRecordset(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/loc123/project/proj123/zone/zone321/recordset", - servermock.ResponseFromFixture("recordset.json")). - Build(t) + client := setupTest(t, http.MethodGet, "/dns/loc123/project/proj123/zone/zone321/recordset", respFromFile("recordset.json")) - recordset, err := client.FindRecordset(t.Context(), "zone321", "SOA", "example.com.") + recordset, err := client.FindRecordset(context.Background(), "zone321", "SOA", "example.com.") require.NoError(t, err) expected := &Recordset{ @@ -63,13 +46,10 @@ func TestClient_CreateRecordset(t *testing.T) { Record: &Record{Content: "value"}, } - client := mockBuilder(). - Route("POST /dns/loc123/project/proj123/zone/zone123/recordset", - servermock.ResponseFromFixture("createRecordset.json"), - servermock.CheckRequestJSONBodyFromStruct(expectedReqBody)). - Build(t) + client := setupTest(t, http.MethodPost, "/dns/loc123/project/proj123/zone/zone123/recordset", + hasReqBody(expectedReqBody), respFromFile("createRecordset.json")) - rs, err := client.CreateRecordset(t.Context(), "zone123", "TXT", "test.example.com.", "value", 3600) + rs, err := client.CreateRecordset(context.Background(), "zone123", "TXT", "test.example.com.", "value", 3600) require.NoError(t, err) expected := &Recordset{RecordType: "TXT", Name: "test.example.com.", TTL: 3600, ID: "1234567890qwertyuiop"} @@ -77,21 +57,16 @@ func TestClient_CreateRecordset(t *testing.T) { } func TestClient_DeleteRecordset(t *testing.T) { - client := mockBuilder(). - Route("DELETE /dns/loc123/project/proj123/zone/zone321/recordset/rs322", nil). - Build(t) + client := setupTest(t, http.MethodDelete, "/dns/loc123/project/proj123/zone/zone321/recordset/rs322") - err := client.DeleteRecordset(t.Context(), "zone321", "rs322") + err := client.DeleteRecordset(context.Background(), "zone321", "rs322") require.NoError(t, err) } func TestClient_GetRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/loc123/project/proj123/zone/321/recordset/322/record", - servermock.ResponseFromFixture("record.json")). - Build(t) + client := setupTest(t, http.MethodGet, "/dns/loc123/project/proj123/zone/321/recordset/322/record", respFromFile("record.json")) - records, err := client.GetRecords(t.Context(), "321", "322") + records, err := client.GetRecords(context.Background(), "321", "322") require.NoError(t, err) expected := []Record{ @@ -110,13 +85,10 @@ func TestClient_CreateRecord(t *testing.T) { Content: "value", } - client := mockBuilder(). - Route("POST /dns/loc123/project/proj123/zone/z123/recordset/rs325/record", - servermock.ResponseFromFixture("createRecord.json"), - servermock.CheckRequestJSONBodyFromStruct(expectedReqBody)). - Build(t) + client := setupTest(t, http.MethodPost, "/dns/loc123/project/proj123/zone/z123/recordset/rs325/record", + hasReqBody(expectedReqBody), respFromFile("createRecord.json")) - rs, err := client.CreateRecord(t.Context(), "z123", "rs325", "value") + rs, err := client.CreateRecord(context.Background(), "z123", "rs325", "value") require.NoError(t, err) expected := &Record{ID: "123321qwerqwewqerq", Content: "value", Enabled: true} @@ -124,22 +96,16 @@ func TestClient_CreateRecord(t *testing.T) { } func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /dns/loc123/project/proj123/zone/321/recordset/322/record/323", - servermock.ResponseFromFixture("createRecord.json")). - Build(t) + client := setupTest(t, http.MethodDelete, "/dns/loc123/project/proj123/zone/321/recordset/322/record/323") - err := client.DeleteRecord(t.Context(), "321", "322", "323") + err := client.DeleteRecord(context.Background(), "321", "322", "323") require.NoError(t, err) } func TestClient_FindZone(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/loc123/project/proj123/zone", - servermock.ResponseFromFixture("zones.json")). - Build(t) + client := setupTest(t, http.MethodGet, "/dns/loc123/project/proj123/zone", respFromFile("zones.json")) - zone, err := client.FindZone(t.Context(), "example.com") + zone, err := client.FindZone(context.Background(), "example.com") require.NoError(t, err) expected := &Zone{ @@ -154,12 +120,9 @@ func TestClient_FindZone(t *testing.T) { } func TestClient_GetZones(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/loc123/project/proj123/zone", - servermock.ResponseFromFixture("zones.json")). - Build(t) + client := setupTest(t, http.MethodGet, "/dns/loc123/project/proj123/zone", respFromFile("zones.json")) - zones, err := client.GetZones(t.Context()) + zones, err := client.GetZones(context.Background()) require.NoError(t, err) expected := []Zone{ @@ -181,3 +144,77 @@ func TestClient_GetZones(t *testing.T) { assert.Equal(t, expected, zones) } + +func setupTest(t *testing.T, method, path string, handlers ...assertHandler) *Client { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.Handle(path, http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + if len(handlers) != 0 { + for _, handler := range handlers { + code, err := handler(rw, req) + if err != nil { + http.Error(rw, err.Error(), code) + return + } + } + } + })) + + passport := &Passport{ + SubjectID: "/iam/project/proj123/sa/xxxxxxx", + } + + client, err := NewClient(server.URL, "loc123", passport) + require.NoError(t, err) + + client.signer = signerMock{} + + return client +} + +type assertHandler func(http.ResponseWriter, *http.Request) (int, error) + +func hasReqBody(v interface{}) assertHandler { + return func(rw http.ResponseWriter, req *http.Request) (int, error) { + reqBody, err := io.ReadAll(req.Body) + if err != nil { + return http.StatusBadRequest, err + } + + marshal, err := json.Marshal(v) + if err != nil { + return http.StatusInternalServerError, err + } + + if !bytes.Equal(marshal, bytes.TrimSpace(reqBody)) { + return http.StatusBadRequest, fmt.Errorf("invalid request body, got: %s, expect: %s", string(reqBody), string(marshal)) + } + + return http.StatusOK, nil + } +} + +func respFromFile(fixtureName string) assertHandler { + return func(rw http.ResponseWriter, req *http.Request) (int, error) { + file, err := os.Open(filepath.Join(".", "fixtures", fixtureName)) + if err != nil { + return http.StatusInternalServerError, err + } + + _, err = io.Copy(rw, file) + if err != nil { + return http.StatusInternalServerError, err + } + + return http.StatusOK, nil + } +} diff --git a/providers/dns/hyperone/internal/passport.go b/providers/dns/hyperone/internal/passport.go index d1503d893..b63236c3b 100644 --- a/providers/dns/hyperone/internal/passport.go +++ b/providers/dns/hyperone/internal/passport.go @@ -25,7 +25,6 @@ func LoadPassportFile(location string) (*Passport, error) { defer func() { _ = file.Close() }() var passport Passport - err = json.NewDecoder(file).Decode(&passport) if err != nil { return nil, fmt.Errorf("failed to parse passport file: %w", err) diff --git a/providers/dns/hyperone/internal/token_test.go b/providers/dns/hyperone/internal/token_test.go index 34b4cc573..243e015e8 100644 --- a/providers/dns/hyperone/internal/token_test.go +++ b/providers/dns/hyperone/internal/token_test.go @@ -1,18 +1,31 @@ package internal import ( - "crypto/rand" - "crypto/rsa" "encoding/base64" "encoding/json" "strings" "testing" - "github.com/go-acme/lego/v4/certcrypto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +const privateKey = `-----BEGIN RSA PRIVATE KEY----- +MIICWgIBAAKBgGFfgMY+DuO8l0RYrMLhcl6U/NigNIiOVhoo/xnYyoQALpWxBaBR ++iVJiBUYunQjKA33yAiY0AasCfSn1JB6asayQvGGn73xztLjkeCVLT+9e4nJ0A/o +dK8SOKBg9FFe70KJrWjJd626el0aVDJjtCE+QxJExA0UZbQp+XIyveQXAgMBAAEC +gYBHcL1XNWLRPaWx9GlUVfoGYMMd4HSKl/ueF+QKP59dt5B2LTnWhS7FOqzH5auu +17hkfx3ZCNzfeEuZn6T6F4bMtsQ6A5iT/DeRlG8tOPiCVZ/L0j6IFM78iIUT8XyA +miwnSy1xGSBA67yUmsLxFg2DtGCjamAkY0C5pccadaB7oQJBAKsIPpMXMni+Oo1I +kVxRyoIZgDxsMJiihG2YLVqo8rPtdErl+Lyg3ziVyg9KR6lFMaTBkYBTLoCPof3E +AB/jyucCQQCRv1cVnYNx+bfnXsBlcsCFDV2HkEuLTpxj7hauD4P3GcyLidSsUkn1 +PiPunZqKpsQaIoxc/BzTOCcP19ifgqdRAkBJ8Cp9FE4xfKt7YJ/WtVVCoRubA3qO +wdNWPa99vgQOXN0lc/3wLevSXo8XxRjtyIgJndT1EQDNe0qglhcnsiaJAkBziAcR +/VAq0tZys2szf6kYTyXqxfj8Lo5NsHeN9oKXJ346xkEtb/VsT5vQFGJishsU1HoL +Y1W+IO7l4iW3G6xhAkACNwtqxSRRbVsNCUMENpKmYhsyN8QXJ8V+o2A9s+pl21Kz +HIIm179mUYCgO6iAHmkqxlFHFwprUBKdPrmP8qF9 +-----END RSA PRIVATE KEY-----` + type Header struct { Algorithm string `json:"alg"` Type string `json:"typ"` @@ -20,10 +33,7 @@ type Header struct { } func TestPayload_buildToken(t *testing.T) { - key, err := rsa.GenerateKey(rand.Reader, 1024) - require.NoError(t, err) - - signer, err := getRSASigner(string(certcrypto.PEMEncode(key)), "sampleKeyId") + signer, err := getRSASigner(privateKey, "sampleKeyId") require.NoError(t, err) payload := Payload{IssuedAt: 1234, Expiry: 4321, Audience: "api.url", Issuer: "issuer", Subject: "subject"} @@ -38,7 +48,6 @@ func TestPayload_buildToken(t *testing.T) { require.NoError(t, err) var headerStruct Header - err = json.Unmarshal(headerString, &headerStruct) require.NoError(t, err) @@ -46,7 +55,6 @@ func TestPayload_buildToken(t *testing.T) { require.NoError(t, err) var payloadStruct Payload - err = json.Unmarshal(payloadString, &payloadStruct) require.NoError(t, err) diff --git a/providers/dns/ibmcloud/ibmcloud.go b/providers/dns/ibmcloud/ibmcloud.go index 81dec8e8b..82d817f19 100644 --- a/providers/dns/ibmcloud/ibmcloud.go +++ b/providers/dns/ibmcloud/ibmcloud.go @@ -6,7 +6,6 @@ import ( "fmt" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/ibmcloud/internal" @@ -33,8 +32,6 @@ const ( EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Username string diff --git a/providers/dns/ibmcloud/ibmcloud.toml b/providers/dns/ibmcloud/ibmcloud.toml index 01088f09b..2a87c5846 100644 --- a/providers/dns/ibmcloud/ibmcloud.toml +++ b/providers/dns/ibmcloud/ibmcloud.toml @@ -7,18 +7,18 @@ Since = "v4.5.0" Example = ''' SOFTLAYER_USERNAME=xxxxx \ SOFTLAYER_API_KEY=yyyyy \ -lego --dns ibmcloud -d '*.example.com' -d example.com run +lego --email you@example.com --dns ibmcloud --domains my.example.org run ''' [Configuration] [Configuration.Credentials] - SOFTLAYER_USERNAME = "Username (IBM Cloud is {accountID}_{emailAddress})" + SOFTLAYER_USERNAME = "Username (IBM Cloud is _)" SOFTLAYER_API_KEY = "Classic Infrastructure API key" [Configuration.Additional] - SOFTLAYER_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - SOFTLAYER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - SOFTLAYER_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - SOFTLAYER_TIMEOUT = "API request timeout in seconds (Default: 30)" + SOFTLAYER_POLLING_INTERVAL = "Time between DNS propagation check" + SOFTLAYER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + SOFTLAYER_TTL = "The TTL of the TXT record used for the DNS challenge" + SOFTLAYER_TIMEOUT = "API request timeout" [Links] API = "https://cloud.ibm.com/docs/dns?topic=dns-getting-started-with-the-dns-api" diff --git a/providers/dns/ibmcloud/ibmcloud_test.go b/providers/dns/ibmcloud/ibmcloud_test.go index 6ca7cd81b..a000e3e59 100644 --- a/providers/dns/ibmcloud/ibmcloud_test.go +++ b/providers/dns/ibmcloud/ibmcloud_test.go @@ -55,7 +55,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -128,7 +127,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -142,7 +140,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/iij/iij.go b/providers/dns/iij/iij.go index 1d098bde2..f5d0fdf9f 100644 --- a/providers/dns/iij/iij.go +++ b/providers/dns/iij/iij.go @@ -6,14 +6,13 @@ import ( "fmt" "slices" "strconv" + "strings" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/iij/doapi" "github.com/iij/doapi/protocol" - "github.com/miekg/dns" ) // Environment variables names. @@ -29,8 +28,6 @@ const ( EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { AccessKey string @@ -98,7 +95,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("iij: %w", err) } - return nil } @@ -111,7 +107,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("iij: %w", err) } - return nil } @@ -228,20 +223,26 @@ func (d *DNSProvider) listZones() ([]string, error) { } func splitDomain(domain string, zones []string) (string, string, error) { - base := dns01.UnFqdn(domain) + parts := strings.Split(strings.Trim(domain, "."), ".") - for _, index := range dns.Split(base) { - zone := base[index:] + var owner string + var zone string + for i := range len(parts) - 1 { + zone = strings.Join(parts[i:], ".") if slices.Contains(zones, zone) { - baseOwner := base[:index] + baseOwner := strings.Join(parts[0:i], ".") if baseOwner != "" { baseOwner = "." + baseOwner } - - return "_acme-challenge" + dns01.UnFqdn(baseOwner), zone, nil + owner = "_acme-challenge" + baseOwner + break } } - return "", "", fmt.Errorf("%s not found", domain) + if owner == "" { + return "", "", fmt.Errorf("%s not found", domain) + } + + return owner, zone, nil } diff --git a/providers/dns/iij/iij.toml b/providers/dns/iij/iij.toml index 95355200a..db0f73309 100644 --- a/providers/dns/iij/iij.toml +++ b/providers/dns/iij/iij.toml @@ -8,7 +8,7 @@ Example = ''' IIJ_API_ACCESS_KEY=xxxxxxxx \ IIJ_API_SECRET_KEY=yyyyyy \ IIJ_DO_SERVICE_CODE=zzzzzz \ -lego --dns iij -d '*.example.com' -d example.com run +lego --email you@example.com --dns iij --domains my.example.org run ''' [Configuration] @@ -17,9 +17,9 @@ lego --dns iij -d '*.example.com' -d example.com run IIJ_API_SECRET_KEY = "API secret key" IIJ_DO_SERVICE_CODE = "DO service code" [Configuration.Additional] - IIJ_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 4)" - IIJ_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 240)" - IIJ_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" + IIJ_POLLING_INTERVAL = "Time between DNS propagation check" + IIJ_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + IIJ_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://manual.iij.jp/p2/pubapi/" diff --git a/providers/dns/iij/iij_test.go b/providers/dns/iij/iij_test.go index bd8140532..936dd9b8d 100644 --- a/providers/dns/iij/iij_test.go +++ b/providers/dns/iij/iij_test.go @@ -71,7 +71,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -162,31 +161,31 @@ func TestSplitDomain(t *testing.T) { }{ { desc: "domain equals zone", - domain: "example.com", - zones: []string{"example.com"}, + domain: "domain.com", + zones: []string{"domain.com"}, expectedOwner: "_acme-challenge", - expectedZone: "example.com", + expectedZone: "domain.com", }, { desc: "with a subdomain", - domain: "my.example.com", - zones: []string{"example.com"}, + domain: "my.domain.com", + zones: []string{"domain.com"}, expectedOwner: "_acme-challenge.my", - expectedZone: "example.com", + expectedZone: "domain.com", }, { desc: "with a subdomain in a zone", - domain: "my.sub.example.com", - zones: []string{"sub.example.com", "example.com"}, + domain: "my.sub.domain.com", + zones: []string{"sub.domain.com", "domain.com"}, expectedOwner: "_acme-challenge.my", - expectedZone: "sub.example.com", + expectedZone: "sub.domain.com", }, { desc: "with a sub-subdomain", - domain: "my.sub.example.com", - zones: []string{"domain1.com", "example.com"}, + domain: "my.sub.domain.com", + zones: []string{"domain1.com", "domain.com"}, expectedOwner: "_acme-challenge.my.sub", - expectedZone: "example.com", + expectedZone: "domain.com", }, } @@ -203,43 +202,12 @@ func TestSplitDomain(t *testing.T) { } } -func TestSplitDomain_error(t *testing.T) { - testCases := []struct { - desc string - domain string - zones []string - expectedOwner string - expectedZone string - }{ - { - desc: "no zone", - domain: "example.com", - zones: nil, - }, - { - desc: "domain does not contain zone", - domain: "example.com", - zones: []string{"example.org"}, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - _, _, err := splitDomain(test.domain, test.zones) - require.Error(t, err) - }) - } -} - func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -253,7 +221,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/iijdpf/iijdpf.go b/providers/dns/iijdpf/iijdpf.go index 2a626e889..a703aeaf2 100644 --- a/providers/dns/iijdpf/iijdpf.go +++ b/providers/dns/iijdpf/iijdpf.go @@ -28,8 +28,6 @@ const ( EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Token string @@ -51,6 +49,8 @@ func NewDefaultConfig() *Config { } } +var _ challenge.Provider = &DNSProvider{} + // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { client dpfapi.ClientInterface diff --git a/providers/dns/iijdpf/iijdpf.toml b/providers/dns/iijdpf/iijdpf.toml index 650285f95..7fa76c04c 100644 --- a/providers/dns/iijdpf/iijdpf.toml +++ b/providers/dns/iijdpf/iijdpf.toml @@ -7,7 +7,7 @@ Since = "v4.7.0" Example = ''' IIJ_DPF_API_TOKEN=xxxxxxxx \ IIJ_DPF_DPM_SERVICE_CODE=yyyyyy \ -lego --dns iijdpf -d '*.example.com' -d example.com run +lego --email you@example.com --dns iijdpf --domains my.example.org run ''' [Configuration] @@ -16,9 +16,9 @@ lego --dns iijdpf -d '*.example.com' -d example.com run IIJ_DPF_DPM_SERVICE_CODE = "IIJ Managed DNS Service's service code" [Configuration.Additional] IIJ_DPF_API_ENDPOINT = "API endpoint URL, defaults to https://api.dns-platform.jp/dpf/v1" - IIJ_DPF_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 5)" - IIJ_DPF_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 660)" - IIJ_DPF_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" + IIJ_DPF_POLLING_INTERVAL = "Time between DNS propagation check, defaults to 5 second" + IIJ_DPF_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation, defaults to 660 second" + IIJ_DPF_TTL = "The TTL of the TXT record used for the DNS challenge, default to 300" [Links] API = "https://manual.iij.jp/dpf/dpfapi/" diff --git a/providers/dns/iijdpf/iijdpf_test.go b/providers/dns/iijdpf/iijdpf_test.go index fbcf3e1f5..a4fa8b8f6 100644 --- a/providers/dns/iijdpf/iijdpf_test.go +++ b/providers/dns/iijdpf/iijdpf_test.go @@ -43,7 +43,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -116,7 +115,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -130,7 +128,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/iijdpf/wrapper.go b/providers/dns/iijdpf/wrapper.go index 0ab26cdcd..12b09a30c 100644 --- a/providers/dns/iijdpf/wrapper.go +++ b/providers/dns/iijdpf/wrapper.go @@ -51,7 +51,6 @@ func (d *DNSProvider) deleteTxtRecord(ctx context.Context, zoneID, fqdn, rdata s // empty target rrset return nil } - return err } @@ -67,13 +66,11 @@ func (d *DNSProvider) deleteTxtRecord(ctx context.Context, zoneID, fqdn, rdata s // delete rdata rdataSlice := dpfzones.RecordRDATASlice{} - for _, v := range r.RData { if v.Value != rdata { rdataSlice = append(rdataSlice, v) } } - r.RData = rdataSlice _, _, err = dpfapiutils.SyncUpdate(ctx, d.client, r, nil) diff --git a/providers/dns/infoblox/infoblox.go b/providers/dns/infoblox/infoblox.go index 054f13679..6eda174d2 100644 --- a/providers/dns/infoblox/infoblox.go +++ b/providers/dns/infoblox/infoblox.go @@ -8,25 +8,22 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" - infoblox "github.com/infobloxopen/infoblox-go-client/v2" + infoblox "github.com/infobloxopen/infoblox-go-client" ) // Environment variables names. const ( envNamespace = "INFOBLOX_" - EnvHost = envNamespace + "HOST" - EnvPort = envNamespace + "PORT" - EnvUsername = envNamespace + "USERNAME" - EnvPassword = envNamespace + "PASSWORD" - EnvDNSView = envNamespace + "DNS_VIEW" - EnvWApiVersion = envNamespace + "WAPI_VERSION" - EnvSSLVerify = envNamespace + "SSL_VERIFY" - EnvCACertificate = envNamespace + "CA_CERTIFICATE" + EnvHost = envNamespace + "HOST" + EnvPort = envNamespace + "PORT" + EnvUsername = envNamespace + "USERNAME" + EnvPassword = envNamespace + "PASSWORD" + EnvDNSView = envNamespace + "DNS_VIEW" + EnvWApiVersion = envNamespace + "WAPI_VERSION" + EnvSSLVerify = envNamespace + "SSL_VERIFY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" @@ -34,9 +31,10 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const defaultPoolConnections = 10 - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) +const ( + defaultPoolConnections = 10 + defaultUserAgent = "go-acme/lego" +) // Config is used to configure the creation of the DNSProvider. type Config struct { @@ -58,9 +56,6 @@ type Config struct { // SSLVerify is whether or not to verify the ssl of the server being hit. SSLVerify bool - // CACertificate is the path to the CA certificate (PEM encoded). - CACertificate string - PropagationTimeout time.Duration PollingInterval time.Duration TTL int @@ -70,11 +65,10 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - DNSView: env.GetOrDefaultString(EnvDNSView, "External"), - WapiVersion: env.GetOrDefaultString(EnvWApiVersion, "2.11"), - Port: env.GetOrDefaultString(EnvPort, "443"), - SSLVerify: env.GetOrDefaultBool(EnvSSLVerify, true), - CACertificate: env.GetOrDefaultString(EnvCACertificate, ""), + DNSView: env.GetOrDefaultString(EnvDNSView, "External"), + WapiVersion: env.GetOrDefaultString(EnvWApiVersion, "2.11"), + Port: env.GetOrDefaultString(EnvPort, "443"), + SSLVerify: env.GetOrDefaultBool(EnvSSLVerify, true), TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), @@ -88,7 +82,6 @@ type DNSProvider struct { config *Config transportConfig infoblox.TransportConfig ibConfig infoblox.HostConfig - ibAuth infoblox.AuthConfig recordRefs map[string]string recordRefsMu sync.Mutex @@ -128,22 +121,13 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("infoblox: missing credentials") } - var sslVerify string - if config.CACertificate != "" { - sslVerify = config.CACertificate - } else { - sslVerify = strconv.FormatBool(config.SSLVerify) - } - return &DNSProvider{ config: config, - transportConfig: infoblox.NewTransportConfig(sslVerify, config.HTTPTimeout, defaultPoolConnections), + transportConfig: infoblox.NewTransportConfig(strconv.FormatBool(config.SSLVerify), config.HTTPTimeout, defaultPoolConnections), ibConfig: infoblox.HostConfig{ - Host: config.Host, - Version: config.WapiVersion, - Port: config.Port, - }, - ibAuth: infoblox.AuthConfig{ + Host: config.Host, + Version: config.WapiVersion, + Port: config.Port, Username: config.Username, Password: config.Password, }, @@ -160,16 +144,16 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) - connector, err := infoblox.NewConnector(d.ibConfig, d.ibAuth, d.transportConfig, &infoblox.WapiRequestBuilder{}, &infoblox.WapiHttpRequestor{}) + connector, err := infoblox.NewConnector(d.ibConfig, d.transportConfig, &infoblox.WapiRequestBuilder{}, &infoblox.WapiHttpRequestor{}) if err != nil { return fmt.Errorf("infoblox: %w", err) } defer func() { _ = connector.Logout() }() - objectManager := infoblox.NewObjectManager(connector, useragent.Get(), "") + objectManager := infoblox.NewObjectManager(connector, defaultUserAgent, "") - record, err := objectManager.CreateTXTRecord(d.config.DNSView, dns01.UnFqdn(info.EffectiveFQDN), info.Value, uint32(d.config.TTL), true, "lego", nil) + record, err := objectManager.CreateTXTRecord(dns01.UnFqdn(info.EffectiveFQDN), info.Value, uint(d.config.TTL), d.config.DNSView) if err != nil { return fmt.Errorf("infoblox: could not create TXT record for %s: %w", domain, err) } @@ -185,20 +169,19 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) - connector, err := infoblox.NewConnector(d.ibConfig, d.ibAuth, d.transportConfig, &infoblox.WapiRequestBuilder{}, &infoblox.WapiHttpRequestor{}) + connector, err := infoblox.NewConnector(d.ibConfig, d.transportConfig, &infoblox.WapiRequestBuilder{}, &infoblox.WapiHttpRequestor{}) if err != nil { return fmt.Errorf("infoblox: %w", err) } defer func() { _ = connector.Logout() }() - objectManager := infoblox.NewObjectManager(connector, useragent.Get(), "") + objectManager := infoblox.NewObjectManager(connector, defaultUserAgent, "") // gets the record's unique ref from when we created it d.recordRefsMu.Lock() recordRef, ok := d.recordRefs[token] d.recordRefsMu.Unlock() - if !ok { return fmt.Errorf("infoblox: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) } diff --git a/providers/dns/infoblox/infoblox.toml b/providers/dns/infoblox/infoblox.toml index 0e6972d3a..761e6f653 100644 --- a/providers/dns/infoblox/infoblox.toml +++ b/providers/dns/infoblox/infoblox.toml @@ -8,7 +8,7 @@ Example = ''' INFOBLOX_USERNAME=api-user-529 \ INFOBLOX_PASSWORD=b9841238feb177a84330febba8a83208921177bffe733 \ INFOBLOX_HOST=infoblox.example.org -lego --dns infoblox -d '*.example.com' -d example.com run +lego --email you@example.com --dns infoblox --domains my.example.org run ''' Additional = ''' @@ -21,15 +21,14 @@ When creating an API's user ensure it has the proper permissions for the view yo INFOBLOX_PASSWORD = "Account Password" INFOBLOX_HOST = "Host URI" [Configuration.Additional] - INFOBLOX_DNS_VIEW = "The view for the TXT records (Default: External)" - INFOBLOX_WAPI_VERSION = "The version of WAPI being used (Default: 2.11)" - INFOBLOX_PORT = "The port for the infoblox grid manager (Default: 443)" - INFOBLOX_SSL_VERIFY = "Whether or not to verify the TLS certificate (Default: true)" - INFOBLOX_CA_CERTIFICATE = "The path to the CA certificate (PEM encoded)" - INFOBLOX_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - INFOBLOX_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - INFOBLOX_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - INFOBLOX_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + INFOBLOX_DNS_VIEW = "The view for the TXT records, default: External" + INFOBLOX_WAPI_VERSION = "The version of WAPI being used, default: 2.11" + INFOBLOX_PORT = "The port for the infoblox grid manager, default: 443" + INFOBLOX_SSL_VERIFY = "Whether or not to verify the TLS certificate, default: true" + INFOBLOX_POLLING_INTERVAL = "Time between DNS propagation check" + INFOBLOX_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + INFOBLOX_TTL = "The TTL of the TXT record used for the DNS challenge" + INFOBLOX_HTTP_TIMEOUT = "HTTP request timeout" [Links] diff --git a/providers/dns/infoblox/infoblox_test.go b/providers/dns/infoblox/infoblox_test.go index 68158cb0d..45434e0e3 100644 --- a/providers/dns/infoblox/infoblox_test.go +++ b/providers/dns/infoblox/infoblox_test.go @@ -68,7 +68,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -150,7 +149,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -164,7 +162,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/infomaniak/infomaniak.go b/providers/dns/infomaniak/infomaniak.go index 9b8b53590..53d98c4f4 100644 --- a/providers/dns/infomaniak/infomaniak.go +++ b/providers/dns/infomaniak/infomaniak.go @@ -9,11 +9,9 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/infomaniak/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Infomaniak API reference: https://api.infomaniak.com/doc @@ -32,8 +30,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIEndpoint string @@ -48,9 +44,9 @@ type Config struct { func NewDefaultConfig() *Config { return &Config{ APIEndpoint: env.GetOrDefaultString(EnvEndpoint, internal.DefaultBaseURL), - TTL: env.GetOrDefaultInt(EnvTTL, 300), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), + TTL: env.GetOrDefaultInt(EnvTTL, 7200), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, @@ -97,11 +93,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("infomaniak: missing access token") } - client, err := internal.New( - clientdebug.Wrap( - internal.OAuthStaticAccessToken(config.HTTPClient, config.AccessToken), - ), - config.APIEndpoint) + client, err := internal.New(internal.OAuthStaticAccessToken(config.HTTPClient, config.AccessToken), config.APIEndpoint) if err != nil { return nil, fmt.Errorf("infomaniak: %w", err) } diff --git a/providers/dns/infomaniak/infomaniak.toml b/providers/dns/infomaniak/infomaniak.toml index d924e3a26..f480ab193 100644 --- a/providers/dns/infomaniak/infomaniak.toml +++ b/providers/dns/infomaniak/infomaniak.toml @@ -6,7 +6,7 @@ Since = "v4.1.0" Example = ''' INFOMANIAK_ACCESS_TOKEN=1234567898765432 \ -lego --dns infomaniak -d '*.example.com' -d example.com run +lego --email you@example.com --dns infomaniak --domains my.example.org run ''' Additional = ''' @@ -21,10 +21,10 @@ You will need domain scope. INFOMANIAK_ACCESS_TOKEN = "Access token" [Configuration.Additional] INFOMANIAK_ENDPOINT = "https://api.infomaniak.com" - INFOMANIAK_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" - INFOMANIAK_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - INFOMANIAK_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - INFOMANIAK_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + INFOMANIAK_POLLING_INTERVAL = "Time between DNS propagation check" + INFOMANIAK_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + INFOMANIAK_TTL = "The TTL of the TXT record used for the DNS challenge in seconds" + INFOMANIAK_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://api.infomaniak.com/doc" diff --git a/providers/dns/infomaniak/infomaniak_test.go b/providers/dns/infomaniak/infomaniak_test.go index 980f3b959..bc8fb7b58 100644 --- a/providers/dns/infomaniak/infomaniak_test.go +++ b/providers/dns/infomaniak/infomaniak_test.go @@ -39,7 +39,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -102,7 +101,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -116,7 +114,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/infomaniak/internal/client.go b/providers/dns/infomaniak/internal/client.go index 40b56c707..886a8966f 100644 --- a/providers/dns/infomaniak/internal/client.go +++ b/providers/dns/infomaniak/internal/client.go @@ -50,7 +50,6 @@ func (c *Client) CreateDNSRecord(ctx context.Context, domain *DNSDomain, record } result := APIResponse[string]{} - err = c.do(req, &result) if err != nil { return "", err @@ -113,7 +112,6 @@ func (c *Client) getDomainByName(ctx context.Context, name string) (*DNSDomain, } result := APIResponse[[]DNSDomain]{} - err = c.do(req, &result) if err != nil { return nil, err diff --git a/providers/dns/infomaniak/internal/client_test.go b/providers/dns/infomaniak/internal/client_test.go index d846f06b4..4fadaf0f5 100644 --- a/providers/dns/infomaniak/internal/client_test.go +++ b/providers/dns/infomaniak/internal/client_test.go @@ -1,34 +1,65 @@ package internal import ( + "bytes" + "context" + "fmt" + "io" + "net/http" "net/http/httptest" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := New(OAuthStaticAccessToken(server.Client(), "token"), server.URL) - if err != nil { - return nil, err - } +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("Bearer token")) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client, err := New(OAuthStaticAccessToken(server.Client(), "token"), server.URL) + require.NoError(t, err) + + return client, mux } func TestClient_CreateDNSRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /1/domain/666/dns/record", - servermock.RawStringResponse(`{"result":"success","data": "123"}`), - servermock.CheckRequestJSONBodyFromFixture("create_dns_record-request.json")). - Build(t) + client, mux := setupTest(t) + + mux.HandleFunc("/1/domain/666/dns/record", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + if req.Header.Get("Authorization") != "Bearer token" { + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + raw, err := io.ReadAll(req.Body) + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + defer func() { _ = req.Body.Close() }() + + if string(bytes.TrimSpace(raw)) != `{"source":"foo","type":"TXT","ttl":60,"target":"txtxtxttxt"}` { + http.Error(rw, fmt.Sprintf("invalid request body: %s", string(raw)), http.StatusBadRequest) + return + } + + response := `{"result":"success","data": "123"}` + + _, err = rw.Write([]byte(response)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) domain := &DNSDomain{ ID: 666, @@ -42,22 +73,62 @@ func TestClient_CreateDNSRecord(t *testing.T) { TTL: 60, } - recordID, err := client.CreateDNSRecord(t.Context(), domain, record) + recordID, err := client.CreateDNSRecord(context.Background(), domain, record) require.NoError(t, err) assert.Equal(t, "123", recordID) } func TestClient_GetDomainByName(t *testing.T) { - client := mockBuilder(). - Route("GET /1/product", - servermock.ResponseFromFixture("get_domain_name.json"), - servermock.CheckQueryParameter().Strict(). - WithRegexp("customer_name", `.+\.example\.com`). - With("service_name", "domain")). - Build(t) + client, mux := setupTest(t) - domain, err := client.GetDomainByName(t.Context(), "one.two.three.example.com.") + mux.HandleFunc("/1/product", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + if req.Header.Get("Authorization") != "Bearer token" { + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + serviceName := req.URL.Query().Get("service_name") + if serviceName != "domain" { + http.Error(rw, fmt.Sprintf("invalid service_name: %s", serviceName), http.StatusBadRequest) + return + } + + customerName := req.URL.Query().Get("customer_name") + if customerName == "" { + http.Error(rw, fmt.Sprintf("invalid customer_name: %s", customerName), http.StatusBadRequest) + return + } + + response := ` + { + "result": "success", + "data": [ + { + "id": 123, + "customer_name": "two.three.example.com" + }, + { + "id": 456, + "customer_name": "three.example.com" + } + ] + } + ` + + _, err := rw.Write([]byte(response)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + domain, err := client.GetDomainByName(context.Background(), "one.two.three.example.com.") require.NoError(t, err) expected := &DNSDomain{ID: 123, CustomerName: "two.three.example.com"} @@ -65,11 +136,26 @@ func TestClient_GetDomainByName(t *testing.T) { } func TestClient_DeleteDNSRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /1/domain/123/dns/record/456", - servermock.RawStringResponse(`{"result":"success"}`)). - Build(t) + client, mux := setupTest(t) - err := client.DeleteDNSRecord(t.Context(), 123, "456") + mux.HandleFunc("/1/domain/123/dns/record/456", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodDelete { + http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + if req.Header.Get("Authorization") != "Bearer token" { + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + _, err := rw.Write([]byte((`{"result":"success"}`))) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + err := client.DeleteDNSRecord(context.Background(), 123, "456") require.NoError(t, err) } diff --git a/providers/dns/infomaniak/internal/fixtures/create_dns_record-request.json b/providers/dns/infomaniak/internal/fixtures/create_dns_record-request.json deleted file mode 100644 index 7e00434f1..000000000 --- a/providers/dns/infomaniak/internal/fixtures/create_dns_record-request.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "source": "foo", - "type": "TXT", - "ttl": 60, - "target": "txtxtxttxt" -} diff --git a/providers/dns/infomaniak/internal/fixtures/get_domain_name.json b/providers/dns/infomaniak/internal/fixtures/get_domain_name.json deleted file mode 100644 index d431cc0d7..000000000 --- a/providers/dns/infomaniak/internal/fixtures/get_domain_name.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "result": "success", - "data": [ - { - "id": 123, - "customer_name": "two.three.example.com" - }, - { - "id": 456, - "customer_name": "three.example.com" - } - ] -} diff --git a/providers/dns/internal/active24/internal/client_test.go b/providers/dns/internal/active24/internal/client_test.go deleted file mode 100644 index f62f78785..000000000 --- a/providers/dns/internal/active24/internal/client_test.go +++ /dev/null @@ -1,182 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - "time" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("example.com", "user", "secret") - if err != nil { - return nil, err - } - - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithRegexp("Authorization", `Basic .+`). - WithRegexp("Date", `\d+-\d+-\d+T\d{2}:\d{2}:\d{2}.*`). - With("Accept-Language", "en_us")) -} - -func TestClient_GetServices(t *testing.T) { - client := mockBuilder(). - Route("GET /v1/user/self/service", - servermock.ResponseFromFixture("services.json")). - Build(t) - - services, err := client.GetServices(t.Context()) - require.NoError(t, err) - - expected := []Service{ - { - ID: 1111, - ServiceName: ".sk doména", - Status: "active", - Name: "mydomain.sk", - CreateTime: 1374357600, - ExpireTime: 1405914526, - Price: 12.3, - }, - { - ID: 2222, - ServiceName: "The Hosting", - Status: "active", - Name: "myname_1", - CreateTime: 1400145443, - ExpireTime: 1431702371, - Price: 55.2, - }, - } - - assert.Equal(t, expected, services) -} - -func TestClient_GetServices_errors(t *testing.T) { - client := mockBuilder(). - Route("GET /v1/user/self/service", - servermock.ResponseFromFixture("error_v1.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) - - _, err := client.GetServices(t.Context()) - require.EqualError(t, err, "401: No username or password.") -} - -func TestClient_GetRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /v2/service/aaa/dns/record", - servermock.ResponseFromFixture("records.json")). - Build(t) - - filter := RecordFilter{ - Name: "example.com", - Type: []string{"TXT"}, - Content: "txt", - } - - records, err := client.GetRecords(t.Context(), "aaa", filter) - require.NoError(t, err) - - expected := []Record{{ - ID: 13, - Name: "string", - Content: "string", - TTL: 120, - Priority: 1, - Port: 443, - Weight: 50, - }} - - assert.Equal(t, expected, records) -} - -func TestClient_GetRecords_errors(t *testing.T) { - client := mockBuilder(). - Route("GET /v2/service/aaa/dns/record", - servermock.ResponseFromFixture("error_403.json"). - WithStatusCode(http.StatusForbidden)). - Build(t) - - filter := RecordFilter{ - Name: "example.com", - Type: []string{"TXT"}, - Content: "txt", - } - - _, err := client.GetRecords(t.Context(), "aaa", filter) - require.EqualError(t, err, "403: /errors/httpException: This action is unauthorized.") -} - -func TestClient_CreateRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /v2/service/aaa/dns/record", - servermock.Noop(). - WithStatusCode(http.StatusNoContent)). - Build(t) - - err := client.CreateRecord(t.Context(), "aaa", Record{}) - require.NoError(t, err) -} - -func TestClient_CreateRecord_errors(t *testing.T) { - client := mockBuilder(). - Route("POST /v2/service/aaa/dns/record", - servermock.ResponseFromFixture("error_403.json"). - WithStatusCode(http.StatusForbidden)). - Build(t) - - err := client.CreateRecord(t.Context(), "aaa", Record{}) - require.EqualError(t, err, "403: /errors/httpException: This action is unauthorized.") -} - -func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /v2/service/aaa/dns/record/123", - servermock.Noop(). - WithStatusCode(http.StatusNoContent)). - Build(t) - - err := client.DeleteRecord(t.Context(), "aaa", "123") - require.NoError(t, err) -} - -func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /v2/service/aaa/dns/record/123", - servermock.ResponseFromFixture("error_403.json"). - WithStatusCode(http.StatusForbidden)). - Build(t) - - err := client.DeleteRecord(t.Context(), "aaa", "123") - require.EqualError(t, err, "403: /errors/httpException: This action is unauthorized.") -} - -func TestClient_sign(t *testing.T) { - client, err := NewClient("example.com", "user", "secret") - require.NoError(t, err) - - req, err := http.NewRequest(http.MethodGet, "/v1/user/self/service", nil) - require.NoError(t, err) - - err = client.sign(req, time.Date(2025, 6, 28, 1, 2, 3, 4, time.UTC)) - require.NoError(t, err) - - username, password, ok := req.BasicAuth() - require.True(t, ok) - - assert.Equal(t, "user", username) - assert.Equal(t, "743e2257421b260ed561f3e7af4b035414636393", password) -} diff --git a/providers/dns/internal/active24/internal/fixtures/error_403.json b/providers/dns/internal/active24/internal/fixtures/error_403.json deleted file mode 100644 index ee3ce196e..000000000 --- a/providers/dns/internal/active24/internal/fixtures/error_403.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "/errors/httpException", - "status": 403, - "title": "This action is unauthorized." -} diff --git a/providers/dns/internal/active24/internal/fixtures/error_422.json b/providers/dns/internal/active24/internal/fixtures/error_422.json deleted file mode 100644 index 0864a1fce..000000000 --- a/providers/dns/internal/active24/internal/fixtures/error_422.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "type": "/errors/validation", - "status": 422, - "title": "The given data was invalid.", - "violations": [ - { - "propertyPath": "string", - "errors": [ - {} - ] - } - ], - "data": { - "name": "Merlin" - } -} diff --git a/providers/dns/internal/active24/internal/fixtures/error_v1.json b/providers/dns/internal/active24/internal/fixtures/error_v1.json deleted file mode 100644 index 8043412e5..000000000 --- a/providers/dns/internal/active24/internal/fixtures/error_v1.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "message": "No username or password.", - "code": 401 -} diff --git a/providers/dns/internal/active24/internal/fixtures/records.json b/providers/dns/internal/active24/internal/fixtures/records.json deleted file mode 100644 index bf07d9ef7..000000000 --- a/providers/dns/internal/active24/internal/fixtures/records.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "currentPage": 0, - "rowsPerPage": 0, - "totalPages": 0, - "totalRecords": 0, - "actions": { - "additionalProp1": { - "additionalProp1": {} - }, - "additionalProp2": { - "additionalProp1": {} - }, - "additionalProp3": { - "additionalProp1": {} - } - }, - "data": [ - { - "id": 13, - "name": "string", - "content": "string", - "ttl": 120, - "priority": 1, - "port": 443, - "weight": 50 - } - ] -} diff --git a/providers/dns/internal/active24/internal/fixtures/services.json b/providers/dns/internal/active24/internal/fixtures/services.json deleted file mode 100644 index ad9b28700..000000000 --- a/providers/dns/internal/active24/internal/fixtures/services.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "items": - [ - { - "id": 1111, - "serviceName": ".sk doména", - "status": "active", - "name": "mydomain.sk", - "createTime": 1374357600, - "expireTime": 1405914526, - "price": 12.3, - "autoExtend": false - }, - { - "id": 2222, - "serviceName": "The Hosting", - "status": "active", - "name": "myname_1", - "createTime": 1400145443, - "expireTime": 1431702371, - "price": 55.2, - "autoExtend": false - } - ], - "pager": - { - "page": 1, - "pagesize": null, - "items": 2 - } -} diff --git a/providers/dns/internal/active24/internal/types.go b/providers/dns/internal/active24/internal/types.go deleted file mode 100644 index ed8dfc9d3..000000000 --- a/providers/dns/internal/active24/internal/types.go +++ /dev/null @@ -1,65 +0,0 @@ -package internal - -import "fmt" - -type APIError struct { - // v2 error - Type string `json:"type,omitempty"` - Status int `json:"status,omitempty"` - Title string `json:"title,omitempty"` - - // v1 error - Message string `json:"message,omitempty"` - Code int `json:"code,omitempty"` -} - -func (a *APIError) Error() string { - if a.Message != "" { - return fmt.Sprintf("%d: %s", a.Code, a.Message) - } - - return fmt.Sprintf("%d: %s: %s", a.Status, a.Type, a.Title) -} - -type APIResponse struct { - Data []Record `json:"data"` -} - -type Record struct { - ID int `json:"id,omitempty"` - Type string `json:"type,omitempty"` - Name string `json:"name,omitempty"` - Content string `json:"content,omitempty"` - TTL int `json:"ttl,omitempty"` - Priority int `json:"priority,omitempty"` - Port int `json:"port,omitempty"` - Weight int `json:"weight,omitempty"` -} - -type OldAPIResponse struct { - Items []Service `json:"items"` -} - -type Service struct { - ID int `json:"id,omitempty"` - ServiceName string `json:"serviceName,omitempty"` - Status string `json:"status,omitempty"` - Name string `json:"name,omitempty"` - CreateTime int `json:"createTime,omitempty"` - ExpireTime int `json:"expireTime,omitempty"` - Price float64 `json:"price,omitempty"` - AutoExtend bool `json:"autoExtend,omitempty"` -} - -type RecordFilter struct { - Name string `json:"name,omitempty"` - Type []string `json:"type,omitempty"` - Content string `json:"content,omitempty"` - TTL int `json:"ttl,omitempty"` - Note string `json:"note,omitempty"` - Priority int `json:"priority,omitempty"` - Port int `json:"port,omitempty"` - Weight int `json:"weight,omitempty"` - Flags int `json:"flags,omitempty"` - Tag []string `json:"tag,omitempty"` -} diff --git a/providers/dns/internal/active24/provider.go b/providers/dns/internal/active24/provider.go deleted file mode 100644 index ae79b8b17..000000000 --- a/providers/dns/internal/active24/provider.go +++ /dev/null @@ -1,179 +0,0 @@ -// Package active24 implements a DNS provider for solving the DNS-01 challenge using Active24. -package active24 - -import ( - "context" - "errors" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/providers/dns/internal/active24/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - Secret string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Active24. -func NewDNSProviderConfig(config *Config, baseAPIDomain string) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(baseAPIDomain, config.APIKey, config.Secret) - if err != nil { - return nil, err - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return err - } - - serviceID, err := d.findServiceID(ctx, dns01.UnFqdn(authZone)) - if err != nil { - return fmt.Errorf("find service ID: %w", err) - } - - record := internal.Record{ - Type: "TXT", - Name: subDomain, - Content: info.Value, - TTL: d.config.TTL, - } - - err = d.client.CreateRecord(ctx, strconv.Itoa(serviceID), record) - if err != nil { - return fmt.Errorf("create record: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("could not find zone for domain %q: %w", domain, err) - } - - serviceID, err := d.findServiceID(ctx, dns01.UnFqdn(authZone)) - if err != nil { - return fmt.Errorf("find service ID: %w", err) - } - - recordID, err := d.findRecordID(ctx, strconv.Itoa(serviceID), info) - if err != nil { - return fmt.Errorf("find record ID: %w", err) - } - - err = d.client.DeleteRecord(ctx, strconv.Itoa(serviceID), strconv.Itoa(recordID)) - if err != nil { - return fmt.Errorf("delete record %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -func (d *DNSProvider) findServiceID(ctx context.Context, domain string) (int, error) { - services, err := d.client.GetServices(ctx) - if err != nil { - return 0, fmt.Errorf("get services: %w", err) - } - - for _, service := range services { - if service.ServiceName != "domain" { - continue - } - - if service.Name != domain { - continue - } - - return service.ID, nil - } - - return 0, fmt.Errorf("service not found for domain: %s", domain) -} - -func (d *DNSProvider) findRecordID(ctx context.Context, serviceID string, info dns01.ChallengeInfo) (int, error) { - // NOTE(ldez): Despite the API documentation, the filter doesn't seem to work. - filter := internal.RecordFilter{ - Name: dns01.UnFqdn(info.EffectiveFQDN), - Type: []string{"TXT"}, - Content: info.Value, - } - - records, err := d.client.GetRecords(ctx, serviceID, filter) - if err != nil { - return 0, fmt.Errorf("get records: %w", err) - } - - for _, record := range records { - if record.Type != "TXT" { - continue - } - - if record.Name != dns01.UnFqdn(info.EffectiveFQDN) { - continue - } - - if record.Content != info.Value { - continue - } - - return record.ID, nil - } - - return 0, errors.New("no record found") -} diff --git a/providers/dns/internal/active24/provider_test.go b/providers/dns/internal/active24/provider_test.go deleted file mode 100644 index e2959fd6e..000000000 --- a/providers/dns/internal/active24/provider_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package active24 - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiKey string - secret string - expected string - }{ - { - desc: "success", - apiKey: "user", - secret: "secret", - }, - { - desc: "missing API key", - apiKey: "", - secret: "secret", - expected: "credentials missing", - }, - { - desc: "missing secret", - apiKey: "user", - secret: "", - expected: "credentials missing", - }, - { - desc: "missing credentials", - expected: "credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := &Config{} - config.APIKey = test.apiKey - config.Secret = test.secret - - p, err := NewDNSProviderConfig(config, "example.com") - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} diff --git a/providers/dns/internal/clientdebug/.gitattributes b/providers/dns/internal/clientdebug/.gitattributes deleted file mode 100644 index 0ce5804f7..000000000 --- a/providers/dns/internal/clientdebug/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -/testdata/** text eol=lf diff --git a/providers/dns/internal/clientdebug/client.go b/providers/dns/internal/clientdebug/client.go deleted file mode 100644 index 342577b93..000000000 --- a/providers/dns/internal/clientdebug/client.go +++ /dev/null @@ -1,134 +0,0 @@ -package clientdebug - -import ( - "fmt" - "io" - "net/http" - "net/http/httputil" - "os" - "regexp" - "strconv" - "strings" - - "github.com/go-acme/lego/v4/platform/config/env" -) - -const replacement = "***" - -type Option func(*DumpTransport) - -func WithEnvKeys(keys ...string) Option { - return func(d *DumpTransport) { - for _, key := range keys { - v := strings.TrimSpace(env.GetOrFile(key)) - if v == "" { - continue - } - - d.replacements = append(d.replacements, v, replacement) - } - } -} - -func WithValues(values ...string) Option { - return func(d *DumpTransport) { - for _, value := range values { - d.replacements = append(d.replacements, value, replacement) - } - } -} - -func WithHeaders(keys ...string) Option { - return func(d *DumpTransport) { - d.regexps = append(d.regexps, - regexp.MustCompile(fmt.Sprintf(`(?im)^(%s):.+$`, strings.Join(keys, "|")))) - } -} - -type DumpTransport struct { - rt http.RoundTripper - - replacements []string - replacer *strings.Replacer - - regexps []*regexp.Regexp - - writer io.Writer -} - -func NewDumpTransport(rt http.RoundTripper, opts ...Option) *DumpTransport { - if rt == nil { - rt = http.DefaultTransport - } - - d := &DumpTransport{ - rt: rt, - writer: os.Stdout, - } - - for _, opt := range opts { - opt(d) - } - - d.regexps = append(d.regexps, - regexp.MustCompile(`(?im)^(Authorization):.+$`), - regexp.MustCompile(`(?im)^(Token|X-Token):.+$`), - regexp.MustCompile(`(?im)^(Auth-Token|X-Auth-Token):.+$`), - regexp.MustCompile(`(?im)^(Api-Key|X-Api-Key|X-Api-Secret):.+$`), - ) - - if len(d.replacements) > 0 { - d.replacer = strings.NewReplacer(d.replacements...) - } - - return d -} - -func (d *DumpTransport) RoundTrip(h *http.Request) (*http.Response, error) { - data, _ := httputil.DumpRequestOut(h, true) - - _, _ = fmt.Fprintln(d.writer, "[HTTP Request]") - _, _ = fmt.Fprintln(d.writer, d.redact(data)) - - resp, err := d.rt.RoundTrip(h) - if err != nil { - return nil, err - } - - data, _ = httputil.DumpResponse(resp, true) - - _, _ = fmt.Fprintln(d.writer, "[HTTP Response]") - _, _ = fmt.Fprintln(d.writer, d.redact(data)) - - return resp, err -} - -func (d *DumpTransport) redact(content []byte) string { - data := string(content) - - for _, r := range d.regexps { - data = r.ReplaceAllString(data, "$1: "+replacement) - } - - if d.replacer == nil { - return data - } - - return d.replacer.Replace(data) -} - -// Wrap wraps an HTTP client Transport with the [DumpTransport]. -func Wrap(client *http.Client, opts ...Option) *http.Client { - val, found := os.LookupEnv("LEGO_DEBUG_DNS_API_HTTP_CLIENT") - if !found { - return client - } - - if ok, _ := strconv.ParseBool(val); !ok { - return client - } - - client.Transport = NewDumpTransport(client.Transport, opts...) - - return client -} diff --git a/providers/dns/internal/clientdebug/client_test.go b/providers/dns/internal/clientdebug/client_test.go deleted file mode 100644 index 3a0c4021a..000000000 --- a/providers/dns/internal/clientdebug/client_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package clientdebug - -import ( - "bytes" - "io" - "net/http" - "net/http/httptest" - "net/url" - "path/filepath" - "strings" - "testing" - "text/template" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestWrap_redact_env_vars(t *testing.T) { - t.Setenv("LEGO_DEBUG_DNS_API_HTTP_CLIENT", "true") - - t.Setenv("MY_VAR_01", "env-aaaa-aaaa") - t.Setenv("MY_VAR_02", "query-aaaa-aaaa") - t.Setenv("MY_VAR_03", "path-aaaa-aaaa") - t.Setenv("MY_VAR_04", "request-body-aaaa-aaaa") - t.Setenv("MY_VAR_05", "request-header-aaaa-aaaa") - t.Setenv("MY_VAR_06", "response-body-aaaa-aaaa") - - buf := bytes.NewBufferString("") - - server, client, req := setupTest(t, buf, - WithEnvKeys("MY_VAR_01", "MY_VAR_02", "MY_VAR_03", "MY_VAR_04", "MY_VAR_05", "MY_VAR_06"), - ) - - now := time.Now() - - resp, err := client.Transport.RoundTrip(req) - require.NoError(t, err) - - assert.Equal(t, http.StatusOK, resp.StatusCode) - - assertDump(t, now, server, buf, "env_vars.txt") -} - -func TestWrap_redact_headers(t *testing.T) { - t.Setenv("LEGO_DEBUG_DNS_API_HTTP_CLIENT", "true") - - buf := bytes.NewBufferString("") - - server, client, req := setupTest(t, buf, - WithHeaders("Secret-Request-Header", "Super-Secret-Request-Header", "Secret-Response-Header"), - ) - - now := time.Now() - - resp, err := client.Transport.RoundTrip(req) - require.NoError(t, err) - - assert.Equal(t, http.StatusOK, resp.StatusCode) - - assertDump(t, now, server, buf, "headers.txt") -} - -func TestWrap_redact_values(t *testing.T) { - t.Setenv("LEGO_DEBUG_DNS_API_HTTP_CLIENT", "true") - - buf := bytes.NewBufferString("") - - server, client, req := setupTest(t, buf, - WithValues("query-aaaa-aaaa", "path-aaaa-aaaa", "request-body-aaaa-aaaa"), - ) - - now := time.Now() - - resp, err := client.Transport.RoundTrip(req) - require.NoError(t, err) - - assert.Equal(t, http.StatusOK, resp.StatusCode) - - assertDump(t, now, server, buf, "values.txt") -} - -func fakeRequest(t *testing.T, baseURL string) *http.Request { - t.Helper() - - endpoint, err := url.Parse(baseURL) - require.NoError(t, err) - - query := endpoint.Query() - query.Set("foo", "query-aaaa-aaaa") - endpoint.RawQuery = query.Encode() - - endpoint = endpoint.JoinPath("path-aaaa-aaaa") - - body := `{ - "foo": "request-body-aaaa-aaaa" -} -` - - req := httptest.NewRequest(http.MethodGet, endpoint.String(), bytes.NewBufferString(body)) - - req.Header.Set("X-Authorization", "not-redacted") - - req.Header.Set("Secret-Request-Header", "request-header-aaaa-aaaa") - req.Header.Set("Super-Secret-Request-Header", "env-aaaa-aaaa") - - req.Header.Set("Authorization", "header-aaaa-0000") - req.Header.Set("Token", "header-aaaa-0001") - req.Header.Set("X-Token", "header-aaaa-0002") - req.Header.Set("Auth-Token", "header-aaaa-0003") - req.Header.Set("X-Auth-Token", "header-aaaa-0004") - req.Header.Set("Api-Key", "header-aaaa-0006") - req.Header.Set("X-Api-Key", "header-aaaa-0007") - req.Header.Set("X-Api-Secret", "header-aaaa-0008") - - req.SetBasicAuth("user", "secret") - - return req -} - -func fakeResponse() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Secret-Response-Header", "response-header-aaaa-aaaa") - _, _ = w.Write([]byte(`{ - "bar": "response-body-aaaa-aaaa" -}`, - )) - } -} - -func withWriter(w io.Writer) Option { - return func(d *DumpTransport) { - if w != nil { - d.writer = w - } - } -} - -func setupTest(t *testing.T, buf io.Writer, opts ...Option) (*httptest.Server, *http.Client, *http.Request) { - t.Helper() - - server := httptest.NewServer(fakeResponse()) - - opts = append(opts, withWriter(buf)) - - client := Wrap(server.Client(), opts...) - - req := fakeRequest(t, server.URL) - - return server, client, req -} - -func assertDump(t *testing.T, now time.Time, server *httptest.Server, actual *bytes.Buffer, filename string) { - t.Helper() - - tmpl, err := template.New(filename).ParseFiles(filepath.Join("testdata", filename)) - require.NoError(t, err) - - expected := bytes.NewBufferString("") - - location, err := time.LoadLocation("GMT") - require.NoError(t, err) - - baseURL, err := url.Parse(server.URL) - require.NoError(t, err) - - err = tmpl.Execute(expected, map[string]string{ - "Host": baseURL.Host, - "Date": now.In(location).Format(time.RFC1123), - }) - require.NoError(t, err) - - assert.Equal(t, expected.String(), strings.ReplaceAll(actual.String(), "\r", "")) -} diff --git a/providers/dns/internal/clientdebug/testdata/env_vars.txt b/providers/dns/internal/clientdebug/testdata/env_vars.txt deleted file mode 100644 index a2697850e..000000000 --- a/providers/dns/internal/clientdebug/testdata/env_vars.txt +++ /dev/null @@ -1,32 +0,0 @@ -[HTTP Request] -GET /***?foo=*** HTTP/1.1 -Host: {{ .Host }} -User-Agent: Go-http-client/1.1 -Content-Length: 37 -Api-Key: *** -Auth-Token: *** -Authorization: *** -Secret-Request-Header: *** -Super-Secret-Request-Header: *** -Token: *** -X-Api-Key: *** -X-Api-Secret: *** -X-Auth-Token: *** -X-Authorization: not-redacted -X-Token: *** -Accept-Encoding: gzip - -{ - "foo": "***" -} - -[HTTP Response] -HTTP/1.1 200 OK -Content-Length: 37 -Content-Type: text/plain; charset=utf-8 -Date: {{ .Date }} -Secret-Response-Header: response-header-aaaa-aaaa - -{ - "bar": "***" -} diff --git a/providers/dns/internal/clientdebug/testdata/headers.txt b/providers/dns/internal/clientdebug/testdata/headers.txt deleted file mode 100644 index fe803fb22..000000000 --- a/providers/dns/internal/clientdebug/testdata/headers.txt +++ /dev/null @@ -1,32 +0,0 @@ -[HTTP Request] -GET /path-aaaa-aaaa?foo=query-aaaa-aaaa HTTP/1.1 -Host: {{ .Host }} -User-Agent: Go-http-client/1.1 -Content-Length: 37 -Api-Key: *** -Auth-Token: *** -Authorization: *** -Secret-Request-Header: *** -Super-Secret-Request-Header: *** -Token: *** -X-Api-Key: *** -X-Api-Secret: *** -X-Auth-Token: *** -X-Authorization: not-redacted -X-Token: *** -Accept-Encoding: gzip - -{ - "foo": "request-body-aaaa-aaaa" -} - -[HTTP Response] -HTTP/1.1 200 OK -Content-Length: 37 -Content-Type: text/plain; charset=utf-8 -Date: {{ .Date }} -Secret-Response-Header: *** - -{ - "bar": "response-body-aaaa-aaaa" -} diff --git a/providers/dns/internal/clientdebug/testdata/values.txt b/providers/dns/internal/clientdebug/testdata/values.txt deleted file mode 100644 index b40f51f14..000000000 --- a/providers/dns/internal/clientdebug/testdata/values.txt +++ /dev/null @@ -1,32 +0,0 @@ -[HTTP Request] -GET /***?foo=*** HTTP/1.1 -Host: {{ .Host }} -User-Agent: Go-http-client/1.1 -Content-Length: 37 -Api-Key: *** -Auth-Token: *** -Authorization: *** -Secret-Request-Header: request-header-aaaa-aaaa -Super-Secret-Request-Header: env-aaaa-aaaa -Token: *** -X-Api-Key: *** -X-Api-Secret: *** -X-Auth-Token: *** -X-Authorization: not-redacted -X-Token: *** -Accept-Encoding: gzip - -{ - "foo": "***" -} - -[HTTP Response] -HTTP/1.1 200 OK -Content-Length: 37 -Content-Type: text/plain; charset=utf-8 -Date: {{ .Date }} -Secret-Response-Header: response-header-aaaa-aaaa - -{ - "bar": "response-body-aaaa-aaaa" -} diff --git a/providers/dns/internal/gcore/internal/client_test.go b/providers/dns/internal/gcore/internal/client_test.go deleted file mode 100644 index 7d70c9308..000000000 --- a/providers/dns/internal/gcore/internal/client_test.go +++ /dev/null @@ -1,165 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const ( - testToken = "test" - testRecordContent = "acme" - testTTL = 10 -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient(testToken) - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders()) -} - -func TestClient_GetZone(t *testing.T) { - expected := Zone{Name: "example.com"} - - client := mockBuilder(). - Route("GET /v2/zones/example.com", - servermock.JSONEncode(expected)). - Build(t) - - zone, err := client.GetZone(t.Context(), "example.com") - require.NoError(t, err) - - assert.Equal(t, expected, zone) -} - -func TestClient_GetZone_error(t *testing.T) { - client := mockBuilder(). - Route("GET /v2/zones/example.com", - servermock.JSONEncode(APIError{Message: "oops"}).WithStatusCode(http.StatusInternalServerError)). - Build(t) - - _, err := client.GetZone(t.Context(), "example.com") - require.EqualError(t, err, "get zone example.com: 500: oops") -} - -func TestClient_GetRRSet(t *testing.T) { - expected := RRSet{ - TTL: testTTL, - Records: []Records{ - {Content: []string{testRecordContent}}, - }, - } - - client := mockBuilder(). - Route("GET /v2/zones/example.com/foo.example.com/TXT", - servermock.JSONEncode(expected)). - Build(t) - - rrSet, err := client.GetRRSet(t.Context(), "example.com", "foo.example.com") - require.NoError(t, err) - - assert.Equal(t, expected, rrSet) -} - -func TestClient_GetRRSet_error(t *testing.T) { - client := mockBuilder(). - Route("GET /v2/zones/example.com/foo.example.com/TXT", - servermock.JSONEncode(APIError{Message: "oops"}).WithStatusCode(http.StatusInternalServerError)). - Build(t) - - _, err := client.GetRRSet(t.Context(), "example.com", "foo.example.com") - require.EqualError(t, err, "get txt records example.com -> foo.example.com: 500: oops") -} - -func TestClient_DeleteRRSet(t *testing.T) { - client := mockBuilder(). - Route("DELETE /v2/zones/test.example.com/my.test.example.com/TXT", nil). - Build(t) - - err := client.DeleteRRSet(t.Context(), "test.example.com", "my.test.example.com.") - require.NoError(t, err) -} - -func TestClient_DeleteRRSet_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /v2/zones/test.example.com/my.test.example.com/TXT", - servermock.JSONEncode(APIError{Message: "oops"}).WithStatusCode(http.StatusInternalServerError)). - Build(t) - - err := client.DeleteRRSet(t.Context(), "test.example.com", "my.test.example.com.") - require.NoError(t, err) -} - -func TestClient_AddRRSet_add(t *testing.T) { - client := mockBuilder(). - // GetRRSet - Route("GET /v2/zones/test.example.com/my.test.example.com/TXT", - servermock.JSONEncode(APIError{Message: "not found"}).WithStatusCode(http.StatusBadRequest)). - // createRRSet - Route("POST /v2/zones/test.example.com/my.test.example.com/TXT", - servermock.JSONEncode([]Records{{Content: []string{testRecordContent}}}), - servermock.CheckRequestJSONBody(`{"ttl":10,"resource_records":[{"content":["acme"]}]}`)). - Build(t) - - err := client.AddRRSet(t.Context(), "test.example.com", "my.test.example.com", testRecordContent, testTTL) - require.NoError(t, err) -} - -func TestClient_AddRRSet_add_error(t *testing.T) { - client := mockBuilder(). - // GetRRSet - Route("GET /v2/zones/test.example.com/my.test.example.com/TXT", - servermock.JSONEncode(APIError{Message: "not found"}).WithStatusCode(http.StatusBadRequest)). - // createRRSet - Route("POST /v2/zones/test.example.com/my.test.example.com/TXT", - servermock.JSONEncode(APIError{Message: "oops"}).WithStatusCode(http.StatusBadRequest)). - Build(t) - - err := client.AddRRSet(t.Context(), "test.example.com", "my.test.example.com", testRecordContent, testTTL) - require.EqualError(t, err, "400: oops") -} - -func TestClient_AddRRSet_update(t *testing.T) { - client := mockBuilder(). - // GetRRSet - Route("GET /v2/zones/test.example.com/my.test.example.com/TXT", - servermock.JSONEncode(RRSet{ - TTL: testTTL, - Records: []Records{{Content: []string{"foo"}}}, - })). - // updateRRSet - Route("PUT /v2/zones/test.example.com/my.test.example.com/TXT", nil, - servermock.CheckRequestJSONBody(`{"ttl":10,"resource_records":[{"content":["acme"]},{"content":["foo"]}]}`)). - Build(t) - - err := client.AddRRSet(t.Context(), "test.example.com", "my.test.example.com", testRecordContent, testTTL) - require.NoError(t, err) -} - -func TestClient_AddRRSet_update_error(t *testing.T) { - client := mockBuilder(). - // GetRRSet - Route("GET /v2/zones/test.example.com/my.test.example.com/TXT", - servermock.JSONEncode(RRSet{ - TTL: testTTL, - Records: []Records{{Content: []string{"foo"}}}, - })). - // updateRRSet - Route("PUT /v2/zones/test.example.com/my.test.example.com/TXT", - servermock.JSONEncode(APIError{Message: "oops"}).WithStatusCode(http.StatusBadRequest)). - Build(t) - - err := client.AddRRSet(t.Context(), "test.example.com", "my.test.example.com", testRecordContent, testTTL) - require.EqualError(t, err, "400: oops") -} diff --git a/providers/dns/internal/gcore/provider.go b/providers/dns/internal/gcore/provider.go deleted file mode 100644 index b2078eba5..000000000 --- a/providers/dns/internal/gcore/provider.go +++ /dev/null @@ -1,126 +0,0 @@ -// Package gcore implements a DNS provider for solving the DNS-01 challenge using G-Core. -package gcore - -import ( - "context" - "errors" - "fmt" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/internal/gcore/internal" -) - -const ( - DefaultPropagationTimeout = 360 * time.Second - DefaultPollingInterval = 20 * time.Second -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config for DNSProvider. -type Config struct { - APIToken string - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// DNSProvider an implementation of challenge.Provider contract. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProviderConfig return a DNSProvider instance configured for G-Core DNS API. -func NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("the configuration of the DNS provider is nil") - } - - if config.APIToken == "" { - return nil, errors.New("incomplete credentials provided") - } - - client := internal.NewClient(config.APIToken) - - if baseURL != "" { - client.BaseURL, _ = url.Parse(baseURL) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record to fulfill the dns-01 challenge. -func (d *DNSProvider) Present(domain, _, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx := context.Background() - - zone, err := d.guessZone(ctx, info.EffectiveFQDN) - if err != nil { - return err - } - - err = d.client.AddRRSet(ctx, zone, dns01.UnFqdn(info.EffectiveFQDN), info.Value, d.config.TTL) - if err != nil { - return fmt.Errorf("add txt record: %w", err) - } - - return nil -} - -// CleanUp removes the record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx := context.Background() - - zone, err := d.guessZone(ctx, info.EffectiveFQDN) - if err != nil { - return err - } - - err = d.client.DeleteRRSet(ctx, zone, dns01.UnFqdn(info.EffectiveFQDN)) - if err != nil { - return fmt.Errorf("remove txt record: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -func (d *DNSProvider) guessZone(ctx context.Context, fqdn string) (string, error) { - var lastErr error - - for zone := range dns01.UnFqdnDomainsSeq(fqdn) { - dnsZone, err := d.client.GetZone(ctx, zone) - if err != nil { - lastErr = err - continue - } - - return dnsZone.Name, nil - } - - return "", fmt.Errorf("zone %q not found: %w", fqdn, lastErr) -} diff --git a/providers/dns/internal/gcore/provider_test.go b/providers/dns/internal/gcore/provider_test.go deleted file mode 100644 index f29dadff9..000000000 --- a/providers/dns/internal/gcore/provider_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package gcore - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiToken string - expected string - }{ - { - desc: "success", - apiToken: "A", - }, - { - desc: "missing credentials", - expected: "incomplete credentials provided", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := &Config{} - config.APIToken = test.apiToken - - p, err := NewDNSProviderConfig(config, "") - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} diff --git a/providers/dns/internal/hostingde/internal/client.go b/providers/dns/internal/hostingde/client.go similarity index 73% rename from providers/dns/internal/hostingde/internal/client.go rename to providers/dns/internal/hostingde/client.go index 133c3479c..8416f202b 100644 --- a/providers/dns/internal/hostingde/internal/client.go +++ b/providers/dns/internal/hostingde/client.go @@ -1,4 +1,4 @@ -package internal +package hostingde import ( "bytes" @@ -10,11 +10,14 @@ import ( "net/url" "time" - "github.com/cenkalti/backoff/v5" + "github.com/cenkalti/backoff/v4" "github.com/go-acme/lego/v4/providers/dns/internal/errutils" ) -const defaultBaseURL = "https://secure.hosting.de/api/dns/v1/json" +const ( + DefaultHostingdeBaseURL = "https://secure.hosting.de/api/dns/v1/json" + DefaultHTTPNetBaseURL = "https://partner.http.net/api/dns/v1/json" +) // Client the API client for Hosting.de. type Client struct { @@ -26,7 +29,7 @@ type Client struct { // NewClient creates new Client. func NewClient(apiKey string) *Client { - baseURL, _ := url.Parse(defaultBaseURL) + baseURL, _ := url.Parse(DefaultHostingdeBaseURL) return &Client{ apiKey: apiKey, @@ -36,31 +39,41 @@ func NewClient(apiKey string) *Client { } // GetZone gets a zone. -func (c *Client) GetZone(ctx context.Context, req ZoneConfigsFindRequest) (*ZoneConfig, error) { - operation := func() (*ZoneConfig, error) { +func (c Client) GetZone(ctx context.Context, req ZoneConfigsFindRequest) (*ZoneConfig, error) { + var zoneConfig *ZoneConfig + + operation := func() error { response, err := c.ListZoneConfigs(ctx, req) if err != nil { - return nil, backoff.Permanent(err) + return backoff.Permanent(err) } if response.Data[0].Status != "active" { - return nil, fmt.Errorf("unexpected status: %q", response.Data[0].Status) + return fmt.Errorf("unexpected status: %q", response.Data[0].Status) } - return &response.Data[0], nil + zoneConfig = &response.Data[0] + + return nil } bo := backoff.NewExponentialBackOff() bo.InitialInterval = 3 * time.Second bo.MaxInterval = 10 * bo.InitialInterval + bo.MaxElapsedTime = 100 * bo.InitialInterval // retry in case the zone was edited recently and is not yet active - return backoff.Retry(ctx, operation, backoff.WithBackOff(bo), backoff.WithMaxElapsedTime(100*bo.InitialInterval)) + err := backoff.Retry(operation, bo) + if err != nil { + return nil, err + } + + return zoneConfig, nil } // ListZoneConfigs lists zone configuration. // https://www.hosting.de/api/?json#list-zoneconfigs -func (c *Client) ListZoneConfigs(ctx context.Context, req ZoneConfigsFindRequest) (*ZoneResponse, error) { +func (c Client) ListZoneConfigs(ctx context.Context, req ZoneConfigsFindRequest) (*ZoneResponse, error) { endpoint := c.BaseURL.JoinPath("zoneConfigsFind") req.AuthToken = c.apiKey @@ -85,7 +98,7 @@ func (c *Client) ListZoneConfigs(ctx context.Context, req ZoneConfigsFindRequest // UpdateZone updates a zone. // https://www.hosting.de/api/?json#updating-zones -func (c *Client) UpdateZone(ctx context.Context, req ZoneUpdateRequest) (*Zone, error) { +func (c Client) UpdateZone(ctx context.Context, req ZoneUpdateRequest) (*Zone, error) { endpoint := c.BaseURL.JoinPath("zoneUpdate") req.AuthToken = c.apiKey @@ -105,7 +118,7 @@ func (c *Client) UpdateZone(ctx context.Context, req ZoneUpdateRequest) (*Zone, return response.Response, nil } -func (c *Client) post(ctx context.Context, endpoint *url.URL, request, result any) ([]byte, error) { +func (c Client) post(ctx context.Context, endpoint *url.URL, request, result any) ([]byte, error) { body, err := json.Marshal(request) if err != nil { return nil, fmt.Errorf("failed to create request JSON body: %w", err) diff --git a/providers/dns/internal/hostingde/internal/client_test.go b/providers/dns/internal/hostingde/client_test.go similarity index 55% rename from providers/dns/internal/hostingde/internal/client_test.go rename to providers/dns/internal/hostingde/client_test.go index d55bbf690..d538c8bc0 100644 --- a/providers/dns/internal/hostingde/internal/client_test.go +++ b/providers/dns/internal/hostingde/client_test.go @@ -1,30 +1,70 @@ -package internal +package hostingde import ( + "bytes" + "context" "encoding/json" + "fmt" + "io" + "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func setupClient(server *httptest.Server) (*Client, error) { +func setupTest(t *testing.T, pattern string, handler http.HandlerFunc) *Client { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + client := NewClient("secret") client.HTTPClient = server.Client() client.BaseURL, _ = url.Parse(server.URL) - return client, nil + mux.HandleFunc(pattern, handler) + + return client +} + +func writeFixture(rw http.ResponseWriter, filename string) { + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, _ = io.Copy(rw, file) } func TestClient_ListZoneConfigs(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("POST /zoneConfigsFind", - servermock.ResponseFromFixture("zoneConfigsFind.json"), - servermock.CheckRequestJSONBodyFromFixture("zoneConfigsFind-request.json")). - Build(t) + client := setupTest(t, "/zoneConfigsFind", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + raw, err := io.ReadAll(req.Body) + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + body := string(bytes.TrimSpace(raw)) + if body != `{"authToken":"secret","filter":{"field":"zoneName","value":"example.com"},"limit":1,"page":1}` { + http.Error(rw, fmt.Sprintf("unexpected body: got %s", body), http.StatusBadRequest) + return + } + + writeFixture(rw, "zoneConfigsFind.json") + }) zonesFind := ZoneConfigsFindRequest{ Filter: Filter{Field: "zoneName", Value: "example.com"}, @@ -32,7 +72,7 @@ func TestClient_ListZoneConfigs(t *testing.T) { Page: 1, } - zoneResponse, err := client.ListZoneConfigs(t.Context(), zonesFind) + zoneResponse, err := client.ListZoneConfigs(context.Background(), zonesFind) require.NoError(t, err) expected := &ZoneResponse{ @@ -69,10 +109,14 @@ func TestClient_ListZoneConfigs(t *testing.T) { } func TestClient_ListZoneConfigs_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("POST /zoneConfigsFind", - servermock.ResponseFromFixture("zoneConfigsFind_error.json")). - Build(t) + client := setupTest(t, "/zoneConfigsFind", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + writeFixture(rw, "zoneConfigsFind_error.json") + }) zonesFind := ZoneConfigsFindRequest{ Filter: Filter{Field: "zoneName", Value: "example.com"}, @@ -80,16 +124,31 @@ func TestClient_ListZoneConfigs_error(t *testing.T) { Page: 1, } - _, err := client.ListZoneConfigs(t.Context(), zonesFind) + _, err := client.ListZoneConfigs(context.Background(), zonesFind) require.Error(t, err) } func TestClient_UpdateZone(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("POST /zoneUpdate", - servermock.ResponseFromFixture("zoneUpdate.json"), - servermock.CheckRequestJSONBodyFromFixture("zoneUpdate-request.json")). - Build(t) + client := setupTest(t, "/zoneUpdate", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + raw, err := io.ReadAll(req.Body) + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + body := string(bytes.TrimSpace(raw)) + if body != `{"authToken":"secret","zoneConfig":{"id":"123","accountId":"456","status":"s","name":"n","nameUnicode":"u","masterIp":"m","type":"t","emailAddress":"e","zoneTransferWhitelist":["a","b"],"lastChangeDate":"l","dnsServerGroupId":"g","dnsSecMode":"m","soaValues":{"refresh":1,"retry":2,"expire":3,"ttl":4,"negativeTtl":5}},"recordsToAdd":null,"recordsToDelete":[{"name":"_acme-challenge.example.com","type":"TXT","content":"\"txt\""}]}` { + http.Error(rw, fmt.Sprintf("unexpected body: got %s", body), http.StatusBadRequest) + return + } + + writeFixture(rw, "zoneUpdate.json") + }) request := ZoneUpdateRequest{ ZoneConfig: ZoneConfig{ @@ -120,7 +179,7 @@ func TestClient_UpdateZone(t *testing.T) { }}, } - response, err := client.UpdateZone(t.Context(), request) + response, err := client.UpdateZone(context.Background(), request) require.NoError(t, err) expected := &Zone{ @@ -162,10 +221,14 @@ func TestClient_UpdateZone(t *testing.T) { } func TestClient_UpdateZone_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("POST /zoneUpdate", - servermock.ResponseFromFixture("zoneUpdate_error.json")). - Build(t) + client := setupTest(t, "/zoneUpdate", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + writeFixture(rw, "zoneUpdate_error.json") + }) request := ZoneUpdateRequest{ ZoneConfig: ZoneConfig{ @@ -196,6 +259,6 @@ func TestClient_UpdateZone_error(t *testing.T) { }}, } - _, err := client.UpdateZone(t.Context(), request) + _, err := client.UpdateZone(context.Background(), request) require.Error(t, err) } diff --git a/providers/dns/internal/hostingde/internal/fixtures/zoneConfigsFind.json b/providers/dns/internal/hostingde/fixtures/zoneConfigsFind.json similarity index 100% rename from providers/dns/internal/hostingde/internal/fixtures/zoneConfigsFind.json rename to providers/dns/internal/hostingde/fixtures/zoneConfigsFind.json diff --git a/providers/dns/internal/hostingde/internal/fixtures/zoneConfigsFind_error.json b/providers/dns/internal/hostingde/fixtures/zoneConfigsFind_error.json similarity index 100% rename from providers/dns/internal/hostingde/internal/fixtures/zoneConfigsFind_error.json rename to providers/dns/internal/hostingde/fixtures/zoneConfigsFind_error.json diff --git a/providers/dns/internal/hostingde/internal/fixtures/zoneUpdate.json b/providers/dns/internal/hostingde/fixtures/zoneUpdate.json similarity index 100% rename from providers/dns/internal/hostingde/internal/fixtures/zoneUpdate.json rename to providers/dns/internal/hostingde/fixtures/zoneUpdate.json diff --git a/providers/dns/internal/hostingde/internal/fixtures/zoneUpdate_error.json b/providers/dns/internal/hostingde/fixtures/zoneUpdate_error.json similarity index 100% rename from providers/dns/internal/hostingde/internal/fixtures/zoneUpdate_error.json rename to providers/dns/internal/hostingde/fixtures/zoneUpdate_error.json diff --git a/providers/dns/internal/hostingde/internal/fixtures/zoneConfigsFind-request.json b/providers/dns/internal/hostingde/internal/fixtures/zoneConfigsFind-request.json deleted file mode 100644 index eb552d9eb..000000000 --- a/providers/dns/internal/hostingde/internal/fixtures/zoneConfigsFind-request.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "authToken": "secret", - "filter": { - "field": "zoneName", - "value": "example.com" - }, - "limit": 1, - "page": 1 -} diff --git a/providers/dns/internal/hostingde/internal/fixtures/zoneUpdate-request.json b/providers/dns/internal/hostingde/internal/fixtures/zoneUpdate-request.json deleted file mode 100644 index 38b1be50d..000000000 --- a/providers/dns/internal/hostingde/internal/fixtures/zoneUpdate-request.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "authToken": "secret", - "zoneConfig": { - "id": "123", - "accountId": "456", - "status": "s", - "name": "n", - "nameUnicode": "u", - "masterIp": "m", - "type": "t", - "emailAddress": "e", - "zoneTransferWhitelist": [ - "a", - "b" - ], - "lastChangeDate": "l", - "dnsServerGroupId": "g", - "dnsSecMode": "m", - "soaValues": { - "refresh": 1, - "retry": 2, - "expire": 3, - "ttl": 4, - "negativeTtl": 5 - } - }, - "recordsToAdd": null, - "recordsToDelete": [ - { - "name": "_acme-challenge.example.com", - "type": "TXT", - "content": "\"txt\"" - } - ] -} diff --git a/providers/dns/internal/hostingde/provider.go b/providers/dns/internal/hostingde/provider.go deleted file mode 100644 index b5277f042..000000000 --- a/providers/dns/internal/hostingde/provider.go +++ /dev/null @@ -1,196 +0,0 @@ -// Package hostingde implements a DNS provider for solving the DNS-01 challenge using hosting.de. -package hostingde - -import ( - "context" - "errors" - "fmt" - "net/http" - "net/url" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/internal/hostingde/internal" -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - ZoneName string - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client - - recordIDs map[string]string - recordIDsMu sync.Mutex -} - -// NewDNSProviderConfig return a DNSProvider instance configured for hosting.de. -func NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("the configuration of the DNS provider is nil") - } - - if config.APIKey == "" { - return nil, errors.New("API key missing") - } - - client := internal.NewClient(config.APIKey) - - if baseURL != "" { - client.BaseURL, _ = url.Parse(baseURL) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - recordIDs: make(map[string]string), - }, nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -// Present creates a TXT record to fulfill the dns-01 challenge. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - zoneName, err := d.getZoneName(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("could not find zone for domain %q: %w", domain, err) - } - - ctx := context.Background() - - // get the ZoneConfig for that domain - zonesFind := internal.ZoneConfigsFindRequest{ - Filter: internal.Filter{Field: "zoneName", Value: zoneName}, - Limit: 1, - Page: 1, - } - - zoneConfig, err := d.client.GetZone(ctx, zonesFind) - if err != nil { - return err - } - - zoneConfig.Name = zoneName - - rec := []internal.DNSRecord{{ - Type: "TXT", - Name: dns01.UnFqdn(info.EffectiveFQDN), - Content: info.Value, - TTL: d.config.TTL, - }} - - req := internal.ZoneUpdateRequest{ - ZoneConfig: *zoneConfig, - RecordsToAdd: rec, - } - - response, err := d.client.UpdateZone(ctx, req) - if err != nil { - return err - } - - for _, record := range response.Records { - if record.Name == dns01.UnFqdn(info.EffectiveFQDN) && record.Content == fmt.Sprintf(`%q`, info.Value) { - d.recordIDsMu.Lock() - d.recordIDs[info.EffectiveFQDN] = record.ID - d.recordIDsMu.Unlock() - } - } - - if d.recordIDs[info.EffectiveFQDN] == "" { - return fmt.Errorf("error getting ID of just created record, for domain %s", domain) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - zoneName, err := d.getZoneName(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("could not find zone for domain %q: %w", domain, err) - } - - ctx := context.Background() - - // get the ZoneConfig for that domain - zonesFind := internal.ZoneConfigsFindRequest{ - Filter: internal.Filter{Field: "zoneName", Value: zoneName}, - Limit: 1, - Page: 1, - } - - zoneConfig, err := d.client.GetZone(ctx, zonesFind) - if err != nil { - return err - } - - zoneConfig.Name = zoneName - - rec := []internal.DNSRecord{{ - Type: "TXT", - Name: dns01.UnFqdn(info.EffectiveFQDN), - Content: `"` + info.Value + `"`, - }} - - req := internal.ZoneUpdateRequest{ - ZoneConfig: *zoneConfig, - RecordsToDelete: rec, - } - - _, err = d.client.UpdateZone(ctx, req) - if err != nil { - return err - } - - // Delete record ID from map - d.recordIDsMu.Lock() - delete(d.recordIDs, info.EffectiveFQDN) - d.recordIDsMu.Unlock() - - return nil -} - -func (d *DNSProvider) getZoneName(fqdn string) (string, error) { - if d.config.ZoneName != "" { - return d.config.ZoneName, nil - } - - zoneName, err := dns01.FindZoneByFqdn(fqdn) - if err != nil { - return "", fmt.Errorf("could not find zone for %s: %w", fqdn, err) - } - - if zoneName == "" { - return "", errors.New("empty zone name") - } - - return dns01.UnFqdn(zoneName), nil -} diff --git a/providers/dns/internal/hostingde/provider_test.go b/providers/dns/internal/hostingde/provider_test.go deleted file mode 100644 index 3cdabf702..000000000 --- a/providers/dns/internal/hostingde/provider_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package hostingde - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiKey string - zoneName string - expected string - }{ - { - desc: "success", - apiKey: "123", - zoneName: "example.org", - }, - { - desc: "missing credentials", - expected: "API key missing", - }, - { - desc: "missing api key", - zoneName: "456", - expected: "API key missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := &Config{} - config.APIKey = test.apiKey - config.ZoneName = test.zoneName - - p, err := NewDNSProviderConfig(config, "") - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.recordIDs) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} diff --git a/providers/dns/internal/hostingde/internal/types.go b/providers/dns/internal/hostingde/types.go similarity index 98% rename from providers/dns/internal/hostingde/internal/types.go rename to providers/dns/internal/hostingde/types.go index 330eab27d..4f3347190 100644 --- a/providers/dns/internal/hostingde/internal/types.go +++ b/providers/dns/internal/hostingde/types.go @@ -1,4 +1,4 @@ -package internal +package hostingde import "encoding/json" @@ -88,8 +88,7 @@ type Zone struct { // https://www.hosting.de/api/?json#updating-zones type ZoneUpdateRequest struct { BaseRequest - ZoneConfig `json:"zoneConfig"` - + ZoneConfig `json:"zoneConfig"` RecordsToAdd []DNSRecord `json:"recordsToAdd"` RecordsToDelete []DNSRecord `json:"recordsToDelete"` } @@ -98,7 +97,6 @@ type ZoneUpdateRequest struct { // https://www.hosting.de/api/?json#list-zoneconfigs type ZoneConfigsFindRequest struct { BaseRequest - Filter Filter `json:"filter"` Limit int `json:"limit"` Page int `json:"page"` diff --git a/providers/dns/internal/ionos/internal/client_test.go b/providers/dns/internal/ionos/internal/client_test.go deleted file mode 100644 index 008d153bc..000000000 --- a/providers/dns/internal/ionos/internal/client_test.go +++ /dev/null @@ -1,162 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("secret") - if err != nil { - return nil, err - } - - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(), - servermock.CheckHeader().With(APIKeyHeader, "secret")) -} - -func TestClient_ListZones(t *testing.T) { - client := mockBuilder(). - Route("GET /v1/zones", - servermock.ResponseFromFixture("list_zones.json")). - Build(t) - - zones, err := client.ListZones(t.Context()) - require.NoError(t, err) - - expected := []Zone{{ - ID: "11af3414-ebba-11e9-8df5-66fbe8a334b4", - Name: "test.com", - Type: "NATIVE", - }} - - assert.Equal(t, expected, zones) -} - -func TestClient_ListZones_error(t *testing.T) { - client := mockBuilder(). - Route("GET /v1/zones", - servermock.ResponseFromFixture("list_zones_error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) - - zones, err := client.ListZones(t.Context()) - require.Error(t, err) - - assert.Nil(t, zones) - - var cErr *ClientError - assert.ErrorAs(t, err, &cErr) - assert.Equal(t, http.StatusUnauthorized, cErr.StatusCode) -} - -func TestClient_GetRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /v1/zones/azone01", - servermock.ResponseFromFixture("get_records.json")). - Build(t) - - records, err := client.GetRecords(t.Context(), "azone01", nil) - require.NoError(t, err) - - expected := []Record{{ - ID: "22af3414-abbe-9e11-5df5-66fbe8e334b4", - Name: "string", - Content: "string", - Type: "A", - }} - - assert.Equal(t, expected, records) -} - -func TestClient_GetRecords_error(t *testing.T) { - client := mockBuilder(). - Route("GET /v1/zones/azone01", - servermock.ResponseFromFixture("get_records_error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) - - records, err := client.GetRecords(t.Context(), "azone01", nil) - require.Error(t, err) - - assert.Nil(t, records) - - var cErr *ClientError - assert.ErrorAs(t, err, &cErr) - assert.Equal(t, http.StatusUnauthorized, cErr.StatusCode) -} - -func TestClient_RemoveRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /v1/zones/azone01/records/arecord01", nil). - Build(t) - - err := client.RemoveRecord(t.Context(), "azone01", "arecord01") - require.NoError(t, err) -} - -func TestClient_RemoveRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /v1/zones/azone01/records/arecord01", - servermock.ResponseFromFixture("remove_record_error.json"). - WithStatusCode(http.StatusInternalServerError)). - Build(t) - - err := client.RemoveRecord(t.Context(), "azone01", "arecord01") - require.Error(t, err) - - var cErr *ClientError - assert.ErrorAs(t, err, &cErr) - assert.Equal(t, http.StatusInternalServerError, cErr.StatusCode) -} - -func TestClient_ReplaceRecords(t *testing.T) { - client := mockBuilder(). - Route("PATCH /v1/zones/azone01", nil). - Build(t) - - records := []Record{{ - ID: "22af3414-abbe-9e11-5df5-66fbe8e334b4", - Name: "string", - Content: "string", - Type: "A", - }} - - err := client.ReplaceRecords(t.Context(), "azone01", records) - require.NoError(t, err) -} - -func TestClient_ReplaceRecords_error(t *testing.T) { - client := mockBuilder(). - Route("PATCH /v1/zones/azone01", - servermock.ResponseFromFixture("replace_records_error.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) - - records := []Record{{ - ID: "22af3414-abbe-9e11-5df5-66fbe8e334b4", - Name: "string", - Content: "string", - Type: "A", - }} - - err := client.ReplaceRecords(t.Context(), "azone01", records) - require.Error(t, err) - - var cErr *ClientError - assert.ErrorAs(t, err, &cErr) - assert.Equal(t, http.StatusBadRequest, cErr.StatusCode) -} diff --git a/providers/dns/internal/ionos/provider.go b/providers/dns/internal/ionos/provider.go deleted file mode 100644 index a7d145840..000000000 --- a/providers/dns/internal/ionos/provider.go +++ /dev/null @@ -1,173 +0,0 @@ -package ionos - -import ( - "context" - "errors" - "fmt" - "net/http" - "net/url" - "strconv" - "strings" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - ionos "github.com/go-acme/lego/v4/providers/dns/internal/ionos/internal" -) - -const MinTTL = 300 - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *ionos.Client -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Ionos. -func NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("the configuration of the DNS provider is nil") - } - - if config.APIKey == "" { - return nil, errors.New("credentials missing") - } - - if config.TTL < MinTTL { - return nil, fmt.Errorf("invalid TTL, TTL (%d) must be greater than %d", config.TTL, MinTTL) - } - - client, err := ionos.NewClient(config.APIKey) - if err != nil { - return nil, err - } - - if baseURL != "" { - client.BaseURL, _ = url.Parse(baseURL) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{config: config, client: client}, nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, _, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx := context.Background() - - zones, err := d.client.ListZones(ctx) - if err != nil { - return fmt.Errorf("failed to get zones: %w", err) - } - - name := dns01.UnFqdn(info.EffectiveFQDN) - - zone := findZone(zones, name) - if zone == nil { - return errors.New("no matching zone found for domain") - } - - filter := &ionos.RecordsFilter{ - Suffix: name, - RecordType: "TXT", - } - - records, err := d.client.GetRecords(ctx, zone.ID, filter) - if err != nil { - return fmt.Errorf("failed to get records (zone=%s): %w", zone.ID, err) - } - - records = append(records, ionos.Record{ - Name: name, - Content: info.Value, - TTL: d.config.TTL, - Type: "TXT", - }) - - err = d.client.ReplaceRecords(ctx, zone.ID, records) - if err != nil { - return fmt.Errorf("failed to create/update records (zone=%s): %w", zone.ID, err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx := context.Background() - - zones, err := d.client.ListZones(ctx) - if err != nil { - return fmt.Errorf("failed to get zones: %w", err) - } - - name := dns01.UnFqdn(info.EffectiveFQDN) - - zone := findZone(zones, name) - if zone == nil { - return errors.New("no matching zone found for domain") - } - - filter := &ionos.RecordsFilter{ - Suffix: name, - RecordType: "TXT", - } - - records, err := d.client.GetRecords(ctx, zone.ID, filter) - if err != nil { - return fmt.Errorf("failed to get records (zone=%s): %w", zone.ID, err) - } - - for _, record := range records { - if record.Name == name && record.Content == strconv.Quote(info.Value) { - err = d.client.RemoveRecord(ctx, zone.ID, record.ID) - if err != nil { - return fmt.Errorf("failed to remove record (zone=%s, record=%s): %w", zone.ID, record.ID, err) - } - - return nil - } - } - - return fmt.Errorf("failed to remove record, record not found (zone=%s, domain=%s, fqdn=%s, value=%s)", zone.ID, domain, info.EffectiveFQDN, info.Value) -} - -func findZone(zones []ionos.Zone, domain string) *ionos.Zone { - var result *ionos.Zone - - for _, zone := range zones { - if zone.Name != "" && strings.HasSuffix(domain, zone.Name) { - if result == nil || len(zone.Name) > len(result.Name) { - result = &zone - } - } - } - - return result -} diff --git a/providers/dns/internal/ionos/provider_test.go b/providers/dns/internal/ionos/provider_test.go deleted file mode 100644 index 6b4df5cc7..000000000 --- a/providers/dns/internal/ionos/provider_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package ionos - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiKey string - tll int - expected string - }{ - { - desc: "success", - apiKey: "123", - tll: MinTTL, - }, - { - desc: "missing credentials", - tll: MinTTL, - expected: "credentials missing", - }, - { - desc: "invalid TTL", - apiKey: "123", - tll: 30, - expected: "invalid TTL, TTL (30) must be greater than 300", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := &Config{} - config.APIKey = test.apiKey - config.TTL = test.tll - - p, err := NewDNSProviderConfig(config, "") - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} diff --git a/providers/dns/internal/ptr/types.go b/providers/dns/internal/ptr/types.go deleted file mode 100644 index b0c7974e0..000000000 --- a/providers/dns/internal/ptr/types.go +++ /dev/null @@ -1,12 +0,0 @@ -package ptr - -func Deref[T any](v *T) T { - if v == nil { - var zero T - return zero - } - - return *v -} - -func Pointer[T any](v T) *T { return &v } diff --git a/providers/dns/internal/rimuhosting/internal/client.go b/providers/dns/internal/rimuhosting/client.go similarity index 86% rename from providers/dns/internal/rimuhosting/internal/client.go rename to providers/dns/internal/rimuhosting/client.go index 5bf7393e7..4976f3781 100644 --- a/providers/dns/internal/rimuhosting/internal/client.go +++ b/providers/dns/internal/rimuhosting/client.go @@ -1,4 +1,4 @@ -package internal +package rimuhosting import ( "context" @@ -15,7 +15,11 @@ import ( querystring "github.com/google/go-querystring/query" ) -const defaultBaseURL = "https://rimuhosting.com/dns/dyndns.jsp" +// Base URL for the RimuHosting DNS services. +const ( + DefaultZonomiBaseURL = "https://zonomi.com/app/dns/dyndns.jsp" + DefaultRimuHostingBaseURL = "https://rimuhosting.com/dns/dyndns.jsp" +) // Action names. const ( @@ -36,7 +40,7 @@ type Client struct { func NewClient(apiKey string) *Client { return &Client{ apiKey: apiKey, - BaseURL: defaultBaseURL, + BaseURL: DefaultZonomiBaseURL, HTTPClient: &http.Client{Timeout: 5 * time.Second}, } } @@ -45,7 +49,7 @@ func NewClient(apiKey string) *Client { // ex: // - https://zonomi.com/app/dns/dyndns.jsp?action=QUERY&name=example.com&api_key=apikeyvaluehere // - https://zonomi.com/app/dns/dyndns.jsp?action=QUERY&name=**.example.com&api_key=apikeyvaluehere -func (c *Client) FindTXTRecords(ctx context.Context, domain string) ([]Record, error) { +func (c Client) FindTXTRecords(ctx context.Context, domain string) ([]Record, error) { action := ActionParameter{ Action: QueryAction, Name: domain, @@ -61,7 +65,7 @@ func (c *Client) FindTXTRecords(ctx context.Context, domain string) ([]Record, e } // DoActions performs actions. -func (c *Client) DoActions(ctx context.Context, actions ...ActionParameter) (*DNSAPIResult, error) { +func (c Client) DoActions(ctx context.Context, actions ...ActionParameter) (*DNSAPIResult, error) { if len(actions) == 0 { return nil, errors.New("no action") } @@ -78,21 +82,18 @@ func (c *Client) DoActions(ctx context.Context, actions ...ActionParameter) (*DN if err != nil { return nil, err } - return resp, nil } multi := c.toMultiParameters(actions) - err := c.do(ctx, multi, resp) if err != nil { return nil, err } - return resp, nil } -func (c *Client) toMultiParameters(params []ActionParameter) multiActionParameter { +func (c Client) toMultiParameters(params []ActionParameter) multiActionParameter { multi := multiActionParameter{ APIKey: c.apiKey, } @@ -108,7 +109,7 @@ func (c *Client) toMultiParameters(params []ActionParameter) multiActionParamete return multi } -func (c *Client) do(ctx context.Context, params, result any) error { +func (c Client) do(ctx context.Context, params, result any) error { baseURL, err := url.Parse(c.BaseURL) if err != nil { return err @@ -159,7 +160,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) errAPI := APIError{} - err := xml.Unmarshal(raw, &errAPI) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/internal/rimuhosting/client_test.go b/providers/dns/internal/rimuhosting/client_test.go new file mode 100644 index 000000000..ecd55b0b5 --- /dev/null +++ b/providers/dns/internal/rimuhosting/client_test.go @@ -0,0 +1,317 @@ +package rimuhosting + +import ( + "context" + "encoding/xml" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient("apikeyvaluehere") + client.BaseURL = server.URL + client.HTTPClient = server.Client() + + return client, mux +} + +func TestClient_FindTXTRecords(t *testing.T) { + client, mux := setupTest(t) + + mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { + query := req.URL.Query() + + var fixture string + switch query.Get("name") { + case "example.com": + fixture = "./fixtures/find_records.xml" + case "**.example.com": + fixture = "./fixtures/find_records_pattern.xml" + default: + fixture = "./fixtures/find_records_empty.xml" + } + + err := writeResponse(rw, fixture) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + testCases := []struct { + desc string + domain string + expected []Record + }{ + { + desc: "simple", + domain: "example.com", + expected: []Record{ + { + Name: "example.org", + Type: "TXT", + Content: "txttxtx", + TTL: "3600 seconds", + Priority: "0", + }, + }, + }, + { + desc: "pattern", + domain: "**.example.com", + expected: []Record{ + { + Name: "_test.example.org", + Type: "TXT", + Content: "txttxtx", + TTL: "3600 seconds", + Priority: "0", + }, + { + Name: "example.org", + Type: "TXT", + Content: "txttxtx", + TTL: "3600 seconds", + Priority: "0", + }, + }, + }, + { + desc: "empty", + domain: "empty.com", + expected: nil, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + records, err := client.FindTXTRecords(context.Background(), test.domain) + require.NoError(t, err) + + assert.Equal(t, test.expected, records) + }) + } +} + +func TestClient_DoActions(t *testing.T) { + type expected struct { + Query string + Resp *DNSAPIResult + Error string + } + + testCases := []struct { + desc string + actions []ActionParameter + fixture string + expected expected + }{ + { + desc: "SET error", + actions: []ActionParameter{ + NewAddRecordAction("example.com", "txttxtx", 0), + }, + fixture: "./fixtures/add_record_error.xml", + expected: expected{ + Query: "action=SET&api_key=apikeyvaluehere&name=example.com&type=TXT&value=txttxtx", + Error: "ERROR: No zone found for example.com", + }, + }, + { + desc: "SET simple", + actions: []ActionParameter{ + NewAddRecordAction("example.org", "txttxtx", 0), + }, + fixture: "./fixtures/add_record.xml", + expected: expected{ + Query: "action=SET&api_key=apikeyvaluehere&name=example.org&type=TXT&value=txttxtx", + Resp: &DNSAPIResult{ + XMLName: xml.Name{Space: "", Local: "dnsapi_result"}, + IsOk: "OK:", + ResultCounts: ResultCounts{Added: "1", Changed: "0", Unchanged: "0", Deleted: "0"}, + Actions: Actions{ + Action: Action{ + Action: "SET", + Host: "example.org", + Type: "TXT", + Records: []Record{{ + Name: "example.org", + Type: "TXT", + Content: "txttxtx", + TTL: "3600 seconds", + Priority: "0", + }}, + }, + }, + }, + }, + }, + { + desc: "SET multiple values", + actions: []ActionParameter{ + NewAddRecordAction("example.org", "txttxtx", 0), + NewAddRecordAction("example.org", "sample", 0), + }, + fixture: "./fixtures/add_record_same_domain.xml", + expected: expected{ + Query: "action[0]=SET&action[1]=SET&api_key=apikeyvaluehere&name[0]=example.org&name[1]=example.org&ttl[0]=0&ttl[1]=0&type[0]=TXT&type[1]=TXT&value[0]=txttxtx&value[1]=sample", + Resp: &DNSAPIResult{ + XMLName: xml.Name{Space: "", Local: "dnsapi_result"}, + IsOk: "OK:", + ResultCounts: ResultCounts{Added: "2", Changed: "0", Unchanged: "0", Deleted: "0"}, + Actions: Actions{ + Action: Action{ + Action: "SET", + Host: "example.org", + Type: "TXT", + Records: []Record{ + { + Name: "example.org", + Type: "TXT", + Content: "txttxtx", + TTL: "0 seconds", + Priority: "0", + }, + { + Name: "example.org", + Type: "TXT", + Content: "sample", + TTL: "0 seconds", + Priority: "0", + }, + }, + }, + }, + }, + }, + }, + { + desc: "DELETE error", + actions: []ActionParameter{ + NewDeleteRecordAction("example.com", "txttxtx"), + }, + fixture: "./fixtures/delete_record_error.xml", + expected: expected{ + Query: "action=DELETE&api_key=apikeyvaluehere&name=example.com&type=TXT&value=txttxtx", + Error: "ERROR: No zone found for example.com", + }, + }, + { + desc: "DELETE nothing", + actions: []ActionParameter{ + NewDeleteRecordAction("example.org", "nothing"), + }, + fixture: "./fixtures/delete_record_nothing.xml", + expected: expected{ + Query: "action=DELETE&api_key=apikeyvaluehere&name=example.org&type=TXT&value=nothing", + Resp: &DNSAPIResult{ + XMLName: xml.Name{Space: "", Local: "dnsapi_result"}, + IsOk: "OK:", + ResultCounts: ResultCounts{Added: "0", Changed: "0", Unchanged: "0", Deleted: "0"}, + Actions: Actions{ + Action: Action{ + Action: "DELETE", + Host: "example.org", + Type: "TXT", + Records: nil, + }, + }, + }, + }, + }, + { + desc: "DELETE simple", + actions: []ActionParameter{ + NewDeleteRecordAction("example.org", "txttxtx"), + }, + fixture: "./fixtures/delete_record.xml", + expected: expected{ + Query: "action=DELETE&api_key=apikeyvaluehere&name=example.org&type=TXT&value=txttxtx", + Resp: &DNSAPIResult{ + XMLName: xml.Name{Space: "", Local: "dnsapi_result"}, + IsOk: "OK:", + ResultCounts: ResultCounts{Added: "0", Changed: "0", Unchanged: "0", Deleted: "1"}, + Actions: Actions{ + Action: Action{ + Action: "DELETE", + Host: "example.org", + Type: "TXT", + Records: []Record{{ + Name: "example.org", + Type: "TXT", + Content: "txttxtx", + TTL: "3600 seconds", + Priority: "0", + }}, + }, + }, + }, + }, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + client, mux := setupTest(t) + + mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { + query, err := url.QueryUnescape(req.URL.RawQuery) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + if test.expected.Query != query { + http.Error(rw, fmt.Sprintf("invalid query: %s", query), http.StatusBadRequest) + return + } + + if test.expected.Error != "" { + rw.WriteHeader(http.StatusInternalServerError) + } + + err = writeResponse(rw, test.fixture) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + resp, err := client.DoActions(context.Background(), test.actions...) + if test.expected.Error != "" { + require.EqualError(t, err, test.expected.Error) + return + } + + require.NoError(t, err) + + assert.Equal(t, test.expected.Resp, resp) + }) + } +} + +func writeResponse(rw io.Writer, filename string) error { + file, err := os.Open(filename) + if err != nil { + return err + } + + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + return err +} diff --git a/providers/dns/internal/rimuhosting/internal/fixtures/add_record.xml b/providers/dns/internal/rimuhosting/fixtures/add_record.xml similarity index 100% rename from providers/dns/internal/rimuhosting/internal/fixtures/add_record.xml rename to providers/dns/internal/rimuhosting/fixtures/add_record.xml diff --git a/providers/dns/internal/rimuhosting/internal/fixtures/add_record_error.xml b/providers/dns/internal/rimuhosting/fixtures/add_record_error.xml similarity index 100% rename from providers/dns/internal/rimuhosting/internal/fixtures/add_record_error.xml rename to providers/dns/internal/rimuhosting/fixtures/add_record_error.xml diff --git a/providers/dns/internal/rimuhosting/internal/fixtures/add_record_same_domain.xml b/providers/dns/internal/rimuhosting/fixtures/add_record_same_domain.xml similarity index 100% rename from providers/dns/internal/rimuhosting/internal/fixtures/add_record_same_domain.xml rename to providers/dns/internal/rimuhosting/fixtures/add_record_same_domain.xml diff --git a/providers/dns/internal/rimuhosting/internal/fixtures/delete_record.xml b/providers/dns/internal/rimuhosting/fixtures/delete_record.xml similarity index 100% rename from providers/dns/internal/rimuhosting/internal/fixtures/delete_record.xml rename to providers/dns/internal/rimuhosting/fixtures/delete_record.xml diff --git a/providers/dns/internal/rimuhosting/internal/fixtures/delete_record_error.xml b/providers/dns/internal/rimuhosting/fixtures/delete_record_error.xml similarity index 100% rename from providers/dns/internal/rimuhosting/internal/fixtures/delete_record_error.xml rename to providers/dns/internal/rimuhosting/fixtures/delete_record_error.xml diff --git a/providers/dns/internal/rimuhosting/internal/fixtures/delete_record_nothing.xml b/providers/dns/internal/rimuhosting/fixtures/delete_record_nothing.xml similarity index 100% rename from providers/dns/internal/rimuhosting/internal/fixtures/delete_record_nothing.xml rename to providers/dns/internal/rimuhosting/fixtures/delete_record_nothing.xml diff --git a/providers/dns/internal/rimuhosting/internal/fixtures/find_records.xml b/providers/dns/internal/rimuhosting/fixtures/find_records.xml similarity index 100% rename from providers/dns/internal/rimuhosting/internal/fixtures/find_records.xml rename to providers/dns/internal/rimuhosting/fixtures/find_records.xml diff --git a/providers/dns/internal/rimuhosting/internal/fixtures/find_records_empty.xml b/providers/dns/internal/rimuhosting/fixtures/find_records_empty.xml similarity index 100% rename from providers/dns/internal/rimuhosting/internal/fixtures/find_records_empty.xml rename to providers/dns/internal/rimuhosting/fixtures/find_records_empty.xml diff --git a/providers/dns/internal/rimuhosting/internal/fixtures/find_records_pattern.xml b/providers/dns/internal/rimuhosting/fixtures/find_records_pattern.xml similarity index 100% rename from providers/dns/internal/rimuhosting/internal/fixtures/find_records_pattern.xml rename to providers/dns/internal/rimuhosting/fixtures/find_records_pattern.xml diff --git a/providers/dns/internal/rimuhosting/internal/client_test.go b/providers/dns/internal/rimuhosting/internal/client_test.go deleted file mode 100644 index 00126dfbe..000000000 --- a/providers/dns/internal/rimuhosting/internal/client_test.go +++ /dev/null @@ -1,332 +0,0 @@ -package internal - -import ( - "encoding/xml" - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func setupClient(server *httptest.Server) (*Client, error) { - client := NewClient("apikeyvaluehere") - client.BaseURL = server.URL - client.HTTPClient = server.Client() - - return client, nil -} - -func TestClient_FindTXTRecords(t *testing.T) { - testCases := []struct { - desc string - domain string - response string - query url.Values - expected []Record - }{ - { - desc: "simple", - domain: "example.com", - response: "find_records.xml", - query: url.Values{ - "name": []string{"example.com"}, - "type": []string{"TXT"}, - "action": []string{"QUERY"}, - "api_key": []string{"apikeyvaluehere"}, - }, - expected: []Record{ - { - Name: "example.org", - Type: "TXT", - Content: "txttxtx", - TTL: "3600 seconds", - Priority: "0", - }, - }, - }, - { - desc: "pattern", - domain: "**.example.com", - response: "find_records_pattern.xml", - query: url.Values{ - "name": []string{"**.example.com"}, - "type": []string{"TXT"}, - "action": []string{"QUERY"}, - "api_key": []string{"apikeyvaluehere"}, - }, - expected: []Record{ - { - Name: "_test.example.org", - Type: "TXT", - Content: "txttxtx", - TTL: "3600 seconds", - Priority: "0", - }, - { - Name: "example.org", - Type: "TXT", - Content: "txttxtx", - TTL: "3600 seconds", - Priority: "0", - }, - }, - }, - { - desc: "empty", - domain: "empty.com", - response: "find_records_empty.xml", - query: url.Values{ - "name": []string{"empty.com"}, - "type": []string{"TXT"}, - "action": []string{"QUERY"}, - "api_key": []string{"apikeyvaluehere"}, - }, - expected: nil, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /", - servermock.ResponseFromFixture(test.response), - servermock.CheckQueryParameter().Strict(). - WithValues(test.query)). - Build(t) - - records, err := client.FindTXTRecords(t.Context(), test.domain) - require.NoError(t, err) - - assert.Equal(t, test.expected, records) - }) - } -} - -func TestClient_DoActions(t *testing.T) { - testCases := []struct { - desc string - actions []ActionParameter - query url.Values - response string - expected *DNSAPIResult - }{ - { - desc: "SET simple", - actions: []ActionParameter{ - NewAddRecordAction("example.org", "txttxtx", 0), - }, - response: "add_record.xml", - query: url.Values{ - "action": []string{"SET"}, - "name": []string{"example.org"}, - "type": []string{"TXT"}, - "value": []string{"txttxtx"}, - "api_key": []string{"apikeyvaluehere"}, - }, - expected: &DNSAPIResult{ - XMLName: xml.Name{Space: "", Local: "dnsapi_result"}, - IsOk: "OK:", - ResultCounts: ResultCounts{Added: "1", Changed: "0", Unchanged: "0", Deleted: "0"}, - Actions: Actions{ - Action: Action{ - Action: "SET", - Host: "example.org", - Type: "TXT", - Records: []Record{{ - Name: "example.org", - Type: "TXT", - Content: "txttxtx", - TTL: "3600 seconds", - Priority: "0", - }}, - }, - }, - }, - }, - { - desc: "SET multiple values", - actions: []ActionParameter{ - NewAddRecordAction("example.org", "txttxtx", 0), - NewAddRecordAction("example.org", "sample", 0), - }, - response: "add_record_same_domain.xml", - query: url.Values{ - "api_key": []string{"apikeyvaluehere"}, - "action[0]": []string{"SET"}, - "name[0]": []string{"example.org"}, - "ttl[0]": []string{"0"}, - "type[0]": []string{"TXT"}, - "value[0]": []string{"txttxtx"}, - "action[1]": []string{"SET"}, - "name[1]": []string{"example.org"}, - "ttl[1]": []string{"0"}, - "type[1]": []string{"TXT"}, - "value[1]": []string{"sample"}, - }, - expected: &DNSAPIResult{ - XMLName: xml.Name{Space: "", Local: "dnsapi_result"}, - IsOk: "OK:", - ResultCounts: ResultCounts{Added: "2", Changed: "0", Unchanged: "0", Deleted: "0"}, - Actions: Actions{ - Action: Action{ - Action: "SET", - Host: "example.org", - Type: "TXT", - Records: []Record{ - { - Name: "example.org", - Type: "TXT", - Content: "txttxtx", - TTL: "0 seconds", - Priority: "0", - }, - { - Name: "example.org", - Type: "TXT", - Content: "sample", - TTL: "0 seconds", - Priority: "0", - }, - }, - }, - }, - }, - }, - { - desc: "DELETE nothing", - actions: []ActionParameter{ - NewDeleteRecordAction("example.org", "nothing"), - }, - response: "delete_record_nothing.xml", - query: url.Values{ - "action": []string{"DELETE"}, - "name": []string{"example.org"}, - "type": []string{"TXT"}, - "value": []string{"nothing"}, - "api_key": []string{"apikeyvaluehere"}, - }, - expected: &DNSAPIResult{ - XMLName: xml.Name{Space: "", Local: "dnsapi_result"}, - IsOk: "OK:", - ResultCounts: ResultCounts{Added: "0", Changed: "0", Unchanged: "0", Deleted: "0"}, - Actions: Actions{ - Action: Action{ - Action: "DELETE", - Host: "example.org", - Type: "TXT", - Records: nil, - }, - }, - }, - }, - { - desc: "DELETE simple", - actions: []ActionParameter{ - NewDeleteRecordAction("example.org", "txttxtx"), - }, - response: "delete_record.xml", - query: url.Values{ - "action": []string{"DELETE"}, - "name": []string{"example.org"}, - "type": []string{"TXT"}, - "value": []string{"txttxtx"}, - "api_key": []string{"apikeyvaluehere"}, - }, - expected: &DNSAPIResult{ - XMLName: xml.Name{Space: "", Local: "dnsapi_result"}, - IsOk: "OK:", - ResultCounts: ResultCounts{Added: "0", Changed: "0", Unchanged: "0", Deleted: "1"}, - Actions: Actions{ - Action: Action{ - Action: "DELETE", - Host: "example.org", - Type: "TXT", - Records: []Record{{ - Name: "example.org", - Type: "TXT", - Content: "txttxtx", - TTL: "3600 seconds", - Priority: "0", - }}, - }, - }, - }, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /", - servermock.ResponseFromFixture(test.response), - servermock.CheckQueryParameter().Strict(). - WithValues(test.query)). - Build(t) - - resp, err := client.DoActions(t.Context(), test.actions...) - require.NoError(t, err) - - assert.Equal(t, test.expected, resp) - }) - } -} - -func TestClient_DoActions_error(t *testing.T) { - testCases := []struct { - desc string - actions []ActionParameter - query url.Values - response string - expected string - }{ - { - desc: "SET error", - actions: []ActionParameter{ - NewAddRecordAction("example.com", "txttxtx", 0), - }, - response: "add_record_error.xml", - query: url.Values{ - "action": []string{"SET"}, - "name": []string{"example.com"}, - "type": []string{"TXT"}, - "value": []string{"txttxtx"}, - "api_key": []string{"apikeyvaluehere"}, - }, - expected: "ERROR: No zone found for example.com", - }, - { - desc: "DELETE error", - actions: []ActionParameter{ - NewDeleteRecordAction("example.com", "txttxtx"), - }, - response: "delete_record_error.xml", - query: url.Values{ - "action": []string{"DELETE"}, - "name": []string{"example.com"}, - "type": []string{"TXT"}, - "value": []string{"txttxtx"}, - "api_key": []string{"apikeyvaluehere"}, - }, - expected: "ERROR: No zone found for example.com", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /", - servermock.ResponseFromFixture(test.response). - WithStatusCode(http.StatusInternalServerError), - servermock.CheckQueryParameter().Strict(). - WithValues(test.query)). - Build(t) - - _, err := client.DoActions(t.Context(), test.actions...) - require.EqualError(t, err, test.expected) - }) - } -} diff --git a/providers/dns/internal/rimuhosting/provider.go b/providers/dns/internal/rimuhosting/provider.go deleted file mode 100644 index 3be764cbf..000000000 --- a/providers/dns/internal/rimuhosting/provider.go +++ /dev/null @@ -1,107 +0,0 @@ -// Package rimuhosting implements a DNS provider for solving the DNS-01 challenge using RimuHosting DNS. -package rimuhosting - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/internal/rimuhosting/internal" -) - -const DefaultTTL = 3600 - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProviderConfig return a DNSProvider instance configured for RimuHosting. -func NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("the configuration of the DNS provider is nil") - } - - if config.APIKey == "" { - return nil, errors.New("incomplete credentials, missing API key") - } - - client := internal.NewClient(config.APIKey) - - if baseURL != "" { - client.BaseURL = baseURL - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{config: config, client: client}, nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx := context.Background() - - records, err := d.client.FindTXTRecords(ctx, dns01.UnFqdn(info.EffectiveFQDN)) - if err != nil { - return fmt.Errorf("failed to find record(s) for %s: %w", domain, err) - } - - actions := []internal.ActionParameter{ - internal.NewAddRecordAction(dns01.UnFqdn(info.EffectiveFQDN), info.Value, d.config.TTL), - } - - for _, record := range records { - actions = append(actions, internal.NewAddRecordAction(record.Name, record.Content, d.config.TTL)) - } - - _, err = d.client.DoActions(ctx, actions...) - if err != nil { - return fmt.Errorf("failed to add record(s) for %s: %w", domain, err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - action := internal.NewDeleteRecordAction(dns01.UnFqdn(info.EffectiveFQDN), info.Value) - - _, err := d.client.DoActions(context.Background(), action) - if err != nil { - return fmt.Errorf("failed to delete record for %s: %w", domain, err) - } - - return nil -} diff --git a/providers/dns/internal/rimuhosting/provider_test.go b/providers/dns/internal/rimuhosting/provider_test.go deleted file mode 100644 index d1569af31..000000000 --- a/providers/dns/internal/rimuhosting/provider_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package rimuhosting - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - expected string - apiKey string - secretKey string - }{ - { - desc: "success", - apiKey: "api_key", - secretKey: "api_secret", - }, - { - desc: "missing api key", - apiKey: "", - secretKey: "api_secret", - expected: "incomplete credentials, missing API key", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := &Config{} - config.APIKey = test.apiKey - - p, err := NewDNSProviderConfig(config, "") - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} diff --git a/providers/dns/internal/rimuhosting/internal/types.go b/providers/dns/internal/rimuhosting/types.go similarity index 98% rename from providers/dns/internal/rimuhosting/internal/types.go rename to providers/dns/internal/rimuhosting/types.go index c3df886a2..bdb333032 100644 --- a/providers/dns/internal/rimuhosting/internal/types.go +++ b/providers/dns/internal/rimuhosting/types.go @@ -1,4 +1,4 @@ -package internal +package rimuhosting import "encoding/xml" diff --git a/providers/dns/internal/selectel/internal/client.go b/providers/dns/internal/selectel/client.go similarity index 91% rename from providers/dns/internal/selectel/internal/client.go rename to providers/dns/internal/selectel/client.go index d441c9894..1e1e4a215 100644 --- a/providers/dns/internal/selectel/internal/client.go +++ b/providers/dns/internal/selectel/client.go @@ -1,4 +1,4 @@ -package internal +package selectel import ( "bytes" @@ -15,11 +15,15 @@ import ( "github.com/go-acme/lego/v4/providers/dns/internal/errutils" ) -const defaultBaseURL = "https://api.selectel.ru/domains/v1" +// Base URL for the Selectel/VScale DNS services. +const ( + DefaultSelectelBaseURL = "https://api.selectel.ru/domains/v1" + DefaultVScaleBaseURL = "https://api.vscale.io/v1/domains" +) const tokenHeader = "X-Token" -// Client represents the DNS client. +// Client represents DNS client. type Client struct { token string @@ -29,7 +33,7 @@ type Client struct { // NewClient returns a client instance. func NewClient(token string) *Client { - baseURL, _ := url.Parse(defaultBaseURL) + baseURL, _ := url.Parse(DefaultVScaleBaseURL) return &Client{ token: token, @@ -48,13 +52,12 @@ func (c *Client) GetDomainByName(ctx context.Context, domainName string) (*Domai } domain := &Domain{} - statusCode, err := c.do(req, domain) if err != nil { if statusCode == http.StatusNotFound && strings.Count(domainName, ".") > 1 { // Look up for the next subdomain - _, after, _ := strings.Cut(domainName, ".") - return c.GetDomainByName(ctx, after) + subIndex := strings.Index(domainName, ".") + return c.GetDomainByName(ctx, domainName[subIndex+1:]) } return nil, err @@ -71,7 +74,6 @@ func (c *Client) AddRecord(ctx context.Context, domainID int, body Record) (*Rec } record := &Record{} - _, err = c.do(req, record) if err != nil { return nil, err @@ -88,7 +90,6 @@ func (c *Client) ListRecords(ctx context.Context, domainID int) ([]Record, error } var records []Record - _, err = c.do(req, &records) if err != nil { return nil, err @@ -107,7 +108,6 @@ func (c *Client) DeleteRecord(ctx context.Context, domainID, recordID int) error } _, err = c.do(req, nil) - return err } @@ -170,7 +170,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) errAPI := &APIError{} - err := json.Unmarshal(raw, errAPI) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/internal/selectel/client_test.go b/providers/dns/internal/selectel/client_test.go new file mode 100644 index 000000000..703fd7b98 --- /dev/null +++ b/providers/dns/internal/selectel/client_test.go @@ -0,0 +1,204 @@ +package selectel + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient("token") + client.BaseURL, _ = url.Parse(server.URL) + client.HTTPClient = server.Client() + + return client, mux +} + +func TestClient_ListRecords(t *testing.T) { + client, mux := setupTest(t) + + mux.HandleFunc("/123/records/", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + fixture := "./fixtures/list_records.json" + + err := writeResponse(rw, fixture) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + records, err := client.ListRecords(context.Background(), 123) + require.NoError(t, err) + + expected := []Record{ + {ID: 123, Name: "example.com", Type: "TXT", TTL: 60, Email: "email@example.com", Content: "txttxttxtA"}, + {ID: 1234, Name: "example.org", Type: "TXT", TTL: 60, Email: "email@example.org", Content: "txttxttxtB"}, + {ID: 12345, Name: "example.net", Type: "TXT", TTL: 60, Email: "email@example.net", Content: "txttxttxtC"}, + } + + assert.Equal(t, expected, records) +} + +func TestClient_ListRecords_error(t *testing.T) { + client, mux := setupTest(t) + + mux.HandleFunc("/123/records/", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + rw.WriteHeader(http.StatusUnauthorized) + err := writeResponse(rw, "./fixtures/error.json") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + records, err := client.ListRecords(context.Background(), 123) + + require.EqualError(t, err, "request failed with status code 401: API error: 400 - error description - field that the error occurred in") + assert.Nil(t, records) +} + +func TestClient_GetDomainByName(t *testing.T) { + client, mux := setupTest(t) + + mux.HandleFunc("/sub.sub.example.org", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + rw.WriteHeader(http.StatusNotFound) + }) + + mux.HandleFunc("/sub.example.org", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + rw.WriteHeader(http.StatusNotFound) + }) + + mux.HandleFunc("/example.org", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + fixture := "./fixtures/domains.json" + + err := writeResponse(rw, fixture) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + domain, err := client.GetDomainByName(context.Background(), "sub.sub.example.org") + require.NoError(t, err) + + expected := &Domain{ + ID: 123, + Name: "example.org", + } + + assert.Equal(t, expected, domain) +} + +func TestClient_AddRecord(t *testing.T) { + client, mux := setupTest(t) + + mux.HandleFunc("/123/records/", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + rec := Record{} + + err := json.NewDecoder(req.Body).Decode(&rec) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + rec.ID = 456 + + err = json.NewEncoder(rw).Encode(rec) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + record, err := client.AddRecord(context.Background(), 123, Record{ + Name: "example.org", + Type: "TXT", + TTL: 60, + Email: "email@example.org", + Content: "txttxttxttxt", + }) + + require.NoError(t, err) + + expected := &Record{ + ID: 456, + Name: "example.org", + Type: "TXT", + TTL: 60, + Email: "email@example.org", + Content: "txttxttxttxt", + } + + assert.Equal(t, expected, record) +} + +func TestClient_DeleteRecord(t *testing.T) { + client, mux := setupTest(t) + + mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodDelete { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + }) + + err := client.DeleteRecord(context.Background(), 123, 456) + require.NoError(t, err) +} + +func writeResponse(rw io.Writer, filename string) error { + file, err := os.Open(filename) + if err != nil { + return err + } + + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + return err +} diff --git a/providers/dns/internal/selectel/internal/fixtures/domains.json b/providers/dns/internal/selectel/fixtures/domains.json similarity index 100% rename from providers/dns/internal/selectel/internal/fixtures/domains.json rename to providers/dns/internal/selectel/fixtures/domains.json diff --git a/providers/dns/internal/selectel/internal/fixtures/error.json b/providers/dns/internal/selectel/fixtures/error.json similarity index 100% rename from providers/dns/internal/selectel/internal/fixtures/error.json rename to providers/dns/internal/selectel/fixtures/error.json diff --git a/providers/dns/internal/selectel/internal/fixtures/list_records.json b/providers/dns/internal/selectel/fixtures/list_records.json similarity index 100% rename from providers/dns/internal/selectel/internal/fixtures/list_records.json rename to providers/dns/internal/selectel/fixtures/list_records.json diff --git a/providers/dns/internal/selectel/internal/client_test.go b/providers/dns/internal/selectel/internal/client_test.go deleted file mode 100644 index edabe0130..000000000 --- a/providers/dns/internal/selectel/internal/client_test.go +++ /dev/null @@ -1,117 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func setupClient(server *httptest.Server) (*Client, error) { - client := NewClient("token") - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil -} - -func TestClient_ListRecords(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient, - servermock.CheckHeader().WithJSONHeaders()). - Route("GET /123/records/", servermock.ResponseFromFixture("list_records.json")). - Build(t) - - records, err := client.ListRecords(t.Context(), 123) - require.NoError(t, err) - - expected := []Record{ - {ID: 123, Name: "example.com", Type: "TXT", TTL: 60, Email: "email@example.com", Content: "txttxttxtA"}, - {ID: 1234, Name: "example.org", Type: "TXT", TTL: 60, Email: "email@example.org", Content: "txttxttxtB"}, - {ID: 12345, Name: "example.net", Type: "TXT", TTL: 60, Email: "email@example.net", Content: "txttxttxtC"}, - } - - assert.Equal(t, expected, records) -} - -func TestClient_ListRecords_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient, - servermock.CheckHeader().WithJSONHeaders(). - With(tokenHeader, "token")). - Route("GET /123/records/", - servermock.ResponseFromFixture("error.json").WithStatusCode(http.StatusUnauthorized)). - Build(t) - - records, err := client.ListRecords(t.Context(), 123) - - require.EqualError(t, err, "request failed with status code 401: API error: 400 - error description - field that the error occurred in") - assert.Nil(t, records) -} - -func TestClient_GetDomainByName(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient, - servermock.CheckHeader().WithJSONHeaders(). - With(tokenHeader, "token")). - Route("GET /sub.sub.example.org", - servermock.Noop().WithStatusCode(http.StatusNotFound)). - Route("GET /sub.example.org", - servermock.Noop().WithStatusCode(http.StatusNotFound)). - Route("GET /example.org", - servermock.ResponseFromFixture("domains.json")). - Build(t) - - domain, err := client.GetDomainByName(t.Context(), "sub.sub.example.org") - require.NoError(t, err) - - expected := &Domain{ - ID: 123, - Name: "example.org", - } - - assert.Equal(t, expected, domain) -} - -func TestClient_AddRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient, - servermock.CheckHeader().WithJSONHeaders(). - With(tokenHeader, "token")). - Route("POST /123/records/", - servermock.ResponseFromFixture("add_record.json"), - servermock.CheckRequestJSONBodyFromFixture("add_record-request.json")). - Build(t) - - record, err := client.AddRecord(t.Context(), 123, Record{ - Name: "example.org", - Type: "TXT", - TTL: 60, - Email: "email@example.org", - Content: "txttxttxttxt", - }) - - require.NoError(t, err) - - expected := &Record{ - ID: 456, - Name: "example.org", - Type: "TXT", - TTL: 60, - Email: "email@example.org", - Content: "txttxttxttxt", - } - - assert.Equal(t, expected, record) -} - -func TestClient_DeleteRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient, - servermock.CheckHeader().WithJSONHeaders(). - With(tokenHeader, "token")). - Route("DELETE /123/records/456", nil). - Build(t) - - err := client.DeleteRecord(t.Context(), 123, 456) - require.NoError(t, err) -} diff --git a/providers/dns/internal/selectel/internal/fixtures/add_record-request.json b/providers/dns/internal/selectel/internal/fixtures/add_record-request.json deleted file mode 100644 index c65d3d267..000000000 --- a/providers/dns/internal/selectel/internal/fixtures/add_record-request.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "example.org", - "type": "TXT", - "ttl": 60, - "email": "email@example.org", - "content": "txttxttxttxt" -} diff --git a/providers/dns/internal/selectel/internal/fixtures/add_record.json b/providers/dns/internal/selectel/internal/fixtures/add_record.json deleted file mode 100644 index 18a436707..000000000 --- a/providers/dns/internal/selectel/internal/fixtures/add_record.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": 456, - "name": "example.org", - "type": "TXT", - "ttl": 60, - "email": "email@example.org", - "content": "txttxttxttxt" -} diff --git a/providers/dns/internal/selectel/provider.go b/providers/dns/internal/selectel/provider.go deleted file mode 100644 index 495735736..000000000 --- a/providers/dns/internal/selectel/provider.go +++ /dev/null @@ -1,137 +0,0 @@ -// Package selectel implements a DNS provider for solving the DNS-01 challenge using Selectel Domains API. -package selectel - -import ( - "context" - "errors" - "fmt" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/internal/selectel/internal" -) - -const MinTTL = 60 - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - Token string - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client - - // TODO(ldez): remove in v5? - BaseURL string -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProviderConfig return a DNSProvider instance configured for selectel. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("the configuration of the DNS provider is nil") - } - - if config.Token == "" { - return nil, errors.New("credentials missing") - } - - if config.TTL < MinTTL { - return nil, fmt.Errorf("invalid TTL, TTL (%d) must be greater than %d", config.TTL, MinTTL) - } - - client := internal.NewClient(config.Token) - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - var err error - - client.BaseURL, err = url.Parse(config.BaseURL) - if err != nil { - return nil, fmt.Errorf("%w", err) - } - - return &DNSProvider{config: config, client: client}, nil -} - -// Timeout returns the Timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -// Present creates a TXT record to fulfill DNS-01 challenge. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx := context.Background() - - // TODO(ldez) replace domain by FQDN to follow CNAME. - domainObj, err := d.client.GetDomainByName(ctx, domain) - if err != nil { - return fmt.Errorf("get domain by name: %w", err) - } - - txtRecord := internal.Record{ - Type: "TXT", - TTL: d.config.TTL, - Name: info.EffectiveFQDN, - Content: info.Value, - } - - _, err = d.client.AddRecord(ctx, domainObj.ID, txtRecord) - if err != nil { - return fmt.Errorf("add record: %w", err) - } - - return nil -} - -// CleanUp removes a TXT record used for DNS-01 challenge. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - recordName := dns01.UnFqdn(info.EffectiveFQDN) - - ctx := context.Background() - - // TODO(ldez) replace domain by FQDN to follow CNAME. - domainObj, err := d.client.GetDomainByName(ctx, domain) - if err != nil { - return fmt.Errorf("%w", err) - } - - records, err := d.client.ListRecords(ctx, domainObj.ID) - if err != nil { - return fmt.Errorf("list records: %w", err) - } - - // Delete records with specific FQDN - var lastErr error - - for _, record := range records { - if record.Name == recordName { - err = d.client.DeleteRecord(ctx, domainObj.ID, record.ID) - if err != nil { - lastErr = fmt.Errorf("delete record: %w", err) - } - } - } - - return lastErr -} diff --git a/providers/dns/internal/selectel/provider_test.go b/providers/dns/internal/selectel/provider_test.go deleted file mode 100644 index 75a032bf4..000000000 --- a/providers/dns/internal/selectel/provider_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package selectel - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - token string - ttl int - expected string - }{ - { - desc: "success", - token: "123", - ttl: 60, - }, - { - desc: "missing api key", - token: "", - ttl: 60, - expected: "credentials missing", - }, - { - desc: "bad TTL value", - token: "123", - ttl: 59, - expected: fmt.Sprintf("invalid TTL, TTL (59) must be greater than %d", MinTTL), - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := &Config{} - config.TTL = test.ttl - config.Token = test.token - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - assert.NotNil(t, p.config) - assert.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} diff --git a/providers/dns/internal/selectel/internal/types.go b/providers/dns/internal/selectel/types.go similarity index 98% rename from providers/dns/internal/selectel/internal/types.go rename to providers/dns/internal/selectel/types.go index e6ca792c0..df7bb3fa7 100644 --- a/providers/dns/internal/selectel/internal/types.go +++ b/providers/dns/internal/selectel/types.go @@ -1,4 +1,4 @@ -package internal +package selectel import "fmt" diff --git a/providers/dns/internal/tecnocratica/internal/client.go b/providers/dns/internal/tecnocratica/internal/client.go deleted file mode 100644 index 5a529fa2f..000000000 --- a/providers/dns/internal/tecnocratica/internal/client.go +++ /dev/null @@ -1,182 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" -) - -// defaultBaseURL is the default API endpoint. -const defaultBaseURL = "https://api.neodigit.net/v1" - -// Client is a Tecnocrática API client. -type Client struct { - token string - - BaseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(token string) (*Client, error) { - if token == "" { - return nil, errors.New("credentials missing: token") - } - - baseURL, err := url.Parse(defaultBaseURL) - if err != nil { - return nil, err - } - - return &Client{ - token: token, - BaseURL: baseURL, - HTTPClient: &http.Client{Timeout: 30 * time.Second}, - }, nil -} - -// GetZones lists all DNS zones. -func (c *Client) GetZones(ctx context.Context) ([]Zone, error) { - endpoint := c.BaseURL.JoinPath("dns", "zones") - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var zones []Zone - - err = c.do(req, &zones) - if err != nil { - return nil, err - } - - return zones, nil -} - -// GetRecords lists all records in a zone. -func (c *Client) GetRecords(ctx context.Context, zoneID int, recordType string) ([]Record, error) { - endpoint := c.BaseURL.JoinPath("dns", "zones", strconv.Itoa(zoneID), "records") - - if recordType != "" { - query := endpoint.Query() - query.Set("type", recordType) - endpoint.RawQuery = query.Encode() - } - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var records []Record - - err = c.do(req, &records) - if err != nil { - return nil, err - } - - return records, nil -} - -// CreateRecord creates a new DNS record. -func (c *Client) CreateRecord(ctx context.Context, zoneID int, record Record) (*Record, error) { - endpoint := c.BaseURL.JoinPath("dns", "zones", strconv.Itoa(zoneID), "records") - - payload := RecordRequest{Record: record} - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, payload) - if err != nil { - return nil, err - } - - var result Record - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return &result, nil -} - -// DeleteRecord deletes a DNS record. -func (c *Client) DeleteRecord(ctx context.Context, zoneID, recordID int) error { - endpoint := c.BaseURL.JoinPath("dns", "zones", strconv.Itoa(zoneID), "records", strconv.Itoa(recordID)) - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return err - } - - return c.do(req, nil) -} - -func (c *Client) do(req *http.Request, result any) error { - useragent.SetHeader(req.Header) - - req.Header.Set("X-TCpanel-Token", c.token) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - raw, _ := io.ReadAll(resp.Body) - - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} diff --git a/providers/dns/internal/tecnocratica/internal/client_test.go b/providers/dns/internal/tecnocratica/internal/client_test.go deleted file mode 100644 index 4e9cf3e85..000000000 --- a/providers/dns/internal/tecnocratica/internal/client_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("secret") - if err != nil { - return nil, err - } - - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - With("X-TCpanel-Token", "secret")) -} - -func TestClient_GetZones(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/zones", - servermock.ResponseFromFixture("get_zones.json")). - Build(t) - - zones, err := client.GetZones(t.Context()) - require.NoError(t, err) - - expected := []Zone{ - { - ID: 6, - Name: "example.com", - HumanName: "example.com", - }, - { - ID: 7, - Name: "example.org", - HumanName: "example.org", - }, - } - - assert.Equal(t, expected, zones) -} - -func TestClient_GetZones_error(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/zones", - servermock.RawStringResponse(`{"error": "unauthorized"}`). - WithStatusCode(http.StatusUnauthorized)). - Build(t) - - zones, err := client.GetZones(t.Context()) - require.Error(t, err) - - assert.Nil(t, zones) -} - -func TestClient_GetRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/zones/6/records", - servermock.ResponseFromFixture("get_records.json")). - Build(t) - - records, err := client.GetRecords(t.Context(), 6, "") - require.NoError(t, err) - - expected := []Record{ - { - ID: 98, - Name: "", - Type: "SOA", - Content: "ns1.example.org dns.example.org 2015092102 7200 7200 1209600 1800", - TTL: 7200, - }, - { - ID: 99, - Name: "", - Type: "NS", - Content: "ns1.example.org", - TTL: 7200, - }, - { - ID: 100, - Name: "_acme-challenge", - Type: "TXT", - Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: 120, - }, - } - - assert.Equal(t, expected, records) -} - -func TestClient_CreateRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /dns/zones/6/records", - servermock.ResponseFromFixture("create_record.json"). - WithStatusCode(http.StatusCreated), - servermock.CheckRequestJSONBodyFromFixture("create_record-request.json")). - Build(t) - - record := Record{ - Name: "_acme-challenge", - Type: "TXT", - Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: 120, - } - - result, err := client.CreateRecord(t.Context(), 6, record) - require.NoError(t, err) - - expected := &Record{ - ID: 101, - Name: "_acme-challenge", - Type: "TXT", - Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: 120, - } - - assert.Equal(t, expected, result) -} - -func TestClient_CreateRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /dns/zones/6/records", - servermock.RawStringResponse(`{"error": "bad request"}`). - WithStatusCode(http.StatusBadRequest)). - Build(t) - - record := Record{ - Name: "_acme-challenge", - Type: "TXT", - Content: "test-value", - TTL: 120, - } - - result, err := client.CreateRecord(t.Context(), 6, record) - require.Error(t, err) - - assert.Nil(t, result) -} - -func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /dns/zones/6/records/101", - servermock.Noop(). - WithStatusCode(http.StatusNoContent)). - Build(t) - - err := client.DeleteRecord(t.Context(), 6, 101) - require.NoError(t, err) -} - -func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /dns/zones/6/records/999", - servermock.RawStringResponse(`{"error": "not found"}`). - WithStatusCode(http.StatusNotFound)). - Build(t) - - err := client.DeleteRecord(t.Context(), 6, 999) - require.Error(t, err) -} diff --git a/providers/dns/internal/tecnocratica/internal/fixtures/create_record-request.json b/providers/dns/internal/tecnocratica/internal/fixtures/create_record-request.json deleted file mode 100644 index 4cd339c98..000000000 --- a/providers/dns/internal/tecnocratica/internal/fixtures/create_record-request.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "record": { - "name": "_acme-challenge", - "type": "TXT", - "content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "ttl": 120 - } -} diff --git a/providers/dns/internal/tecnocratica/internal/fixtures/create_record.json b/providers/dns/internal/tecnocratica/internal/fixtures/create_record.json deleted file mode 100644 index 6f30010ac..000000000 --- a/providers/dns/internal/tecnocratica/internal/fixtures/create_record.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "id": 101, - "name": "_acme-challenge", - "type": "TXT", - "content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "ttl": 120, - "prio": null, - "created_at": "2015-09-21T14:40:27.127+02:00", - "updated_at": "2015-09-21T14:40:27.127+02:00" -} diff --git a/providers/dns/internal/tecnocratica/internal/fixtures/get_records.json b/providers/dns/internal/tecnocratica/internal/fixtures/get_records.json deleted file mode 100644 index 00e09c37f..000000000 --- a/providers/dns/internal/tecnocratica/internal/fixtures/get_records.json +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - "id": 98, - "name": "", - "type": "SOA", - "content": "ns1.example.org dns.example.org 2015092102 7200 7200 1209600 1800", - "ttl": 7200, - "prio": null - }, - { - "id": 99, - "name": "", - "type": "NS", - "content": "ns1.example.org", - "ttl": 7200, - "prio": null - }, - { - "id": 100, - "name": "_acme-challenge", - "type": "TXT", - "content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "ttl": 120, - "prio": null - } -] diff --git a/providers/dns/internal/tecnocratica/internal/fixtures/get_zones.json b/providers/dns/internal/tecnocratica/internal/fixtures/get_zones.json deleted file mode 100644 index 01a08dced..000000000 --- a/providers/dns/internal/tecnocratica/internal/fixtures/get_zones.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "id": 6, - "name": "example.com", - "created_at": "2015-09-21T12:19:04.000+02:00", - "updated_at": "2015-09-21T12:19:04.000+02:00", - "human_name": "example.com" - }, - { - "id": 7, - "name": "example.org", - "created_at": "2015-09-22T10:00:00.000+02:00", - "updated_at": "2015-09-22T10:00:00.000+02:00", - "human_name": "example.org" - } -] diff --git a/providers/dns/internal/tecnocratica/internal/types.go b/providers/dns/internal/tecnocratica/internal/types.go deleted file mode 100644 index 505bfbced..000000000 --- a/providers/dns/internal/tecnocratica/internal/types.go +++ /dev/null @@ -1,23 +0,0 @@ -package internal - -// Zone represents a DNS zone. -type Zone struct { - ID int `json:"id"` - Name string `json:"name"` - HumanName string `json:"human_name"` -} - -// Record represents a DNS record. -type Record struct { - ID int `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` - Content string `json:"content,omitempty"` - TTL int `json:"ttl,omitempty"` - Priority int `json:"prio,omitempty"` -} - -// RecordRequest is the request body for creating/updating a record. -type RecordRequest struct { - Record Record `json:"record"` -} diff --git a/providers/dns/internal/tecnocratica/provider.go b/providers/dns/internal/tecnocratica/provider.go deleted file mode 100644 index 17cfb8379..000000000 --- a/providers/dns/internal/tecnocratica/provider.go +++ /dev/null @@ -1,165 +0,0 @@ -// Package tecnocratica implements a DNS provider for solving the DNS-01 challenge using Tecnocrática. -package tecnocratica - -import ( - "context" - "errors" - "fmt" - "net/http" - "net/url" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/internal/tecnocratica/internal" -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - Token string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client - - zoneIDs map[string]int - recordIDs map[string]int - recordIDsMu sync.Mutex -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Tecnocrática. -func NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("the configuration of the DNS provider is nil") - } - - if config.Token == "" { - return nil, errors.New("missing credentials") - } - - client, err := internal.NewClient(config.Token) - if err != nil { - return nil, fmt.Errorf("create client: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - if baseURL != "" { - client.BaseURL, err = url.Parse(baseURL) - if err != nil { - return nil, err - } - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - zoneIDs: make(map[string]int), - recordIDs: make(map[string]int), - }, nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("could not find zone for domain %q: %w", domain, err) - } - - authZone = dns01.UnFqdn(authZone) - - zone, err := d.findZone(ctx, authZone) - if err != nil { - return fmt.Errorf("%w", err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("%w", err) - } - - record := internal.Record{ - Name: subDomain, - Type: "TXT", - Content: info.Value, - TTL: d.config.TTL, - } - - newRecord, err := d.client.CreateRecord(ctx, zone.ID, record) - if err != nil { - return fmt.Errorf("create record: %w", err) - } - - d.recordIDsMu.Lock() - d.zoneIDs[token] = zone.ID - d.recordIDs[token] = newRecord.ID - d.recordIDsMu.Unlock() - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - d.recordIDsMu.Lock() - zoneID, zoneOK := d.zoneIDs[token] - recordID, recordOK := d.recordIDs[token] - d.recordIDsMu.Unlock() - - if !zoneOK || !recordOK { - return fmt.Errorf("unknown record ID or zone ID for '%s' '%s'", info.EffectiveFQDN, token) - } - - err := d.client.DeleteRecord(context.Background(), zoneID, recordID) - if err != nil { - return fmt.Errorf("delete record: fqdn=%s, zoneID=%d, recordID=%d: %w", - info.EffectiveFQDN, zoneID, recordID, err) - } - - d.recordIDsMu.Lock() - delete(d.zoneIDs, token) - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - - return nil -} - -func (d *DNSProvider) findZone(ctx context.Context, zoneName string) (*internal.Zone, error) { - zones, err := d.client.GetZones(ctx) - if err != nil { - return nil, fmt.Errorf("get zones: %w", err) - } - - for _, zone := range zones { - if zone.Name == zoneName || zone.HumanName == zoneName { - return &zone, nil - } - } - - return nil, fmt.Errorf("zone not found: %s", zoneName) -} diff --git a/providers/dns/internal/tecnocratica/provider_test.go b/providers/dns/internal/tecnocratica/provider_test.go deleted file mode 100644 index 33e5f7c67..000000000 --- a/providers/dns/internal/tecnocratica/provider_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package tecnocratica - -import ( - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - token string - expected string - }{ - { - desc: "success", - token: "secret", - }, - { - desc: "missing token", - expected: "missing credentials", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := &Config{} - config.Token = test.token - - p, err := NewDNSProviderConfig(config, "") - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := &Config{ - Token: "secret", - PropagationTimeout: 10 * time.Second, - PollingInterval: 1 * time.Second, - TTL: 120, - HTTPClient: server.Client(), - } - - p, err := NewDNSProviderConfig(config, server.URL) - if err != nil { - return nil, err - } - - return p, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - With("X-TCpanel-Token", "secret"), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("GET /dns/zones", - servermock.ResponseFromInternal("get_zones.json")). - Route("POST /dns/zones/6/records", - servermock.ResponseFromInternal("create_record.json"). - WithStatusCode(http.StatusCreated), - servermock.CheckRequestJSONBodyFromInternal("create_record-request.json")). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("DELETE /dns/zones/456/records/123", - servermock.Noop(). - WithStatusCode(http.StatusNoContent)). - Build(t) - - token := "abc" - - provider.recordIDs[token] = 123 - provider.zoneIDs[token] = 456 - - err := provider.CleanUp("example.com", token, "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/internal/useragent/useragent.go b/providers/dns/internal/useragent/useragent.go deleted file mode 100644 index 090c9109a..000000000 --- a/providers/dns/internal/useragent/useragent.go +++ /dev/null @@ -1,29 +0,0 @@ -// Code generated by 'internal/releaser'; DO NOT EDIT. - -package useragent - -import ( - "fmt" - "net/http" - "runtime" -) - -const ( - // ourUserAgent is the User-Agent of this underlying library package. - ourUserAgent = "goacme-lego/4.32.0" - - // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. - // values: detach|release - // NOTE: Update this with each tagged release. - ourUserAgentComment = "detach" -) - -// Get builds and returns the User-Agent string. -func Get() string { - return fmt.Sprintf("%s (%s; %s; %s)", ourUserAgent, ourUserAgentComment, runtime.GOOS, runtime.GOARCH) -} - -// SetHeader sets the User-Agent header. -func SetHeader(h http.Header) { - h.Set("User-Agent", Get()) -} diff --git a/providers/dns/internal/westcn/internal/client.go b/providers/dns/internal/westcn/internal/client.go deleted file mode 100644 index 621c7865f..000000000 --- a/providers/dns/internal/westcn/internal/client.go +++ /dev/null @@ -1,211 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "crypto/md5" - "encoding/hex" - "encoding/json" - "errors" - "io" - "net/http" - "net/url" - "strconv" - "strings" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - querystring "github.com/google/go-querystring/query" - "golang.org/x/text/encoding" - "golang.org/x/text/encoding/simplifiedchinese" - "golang.org/x/text/transform" -) - -const defaultBaseURL = "https://api.west.cn/api/v2" - -// Client the West.cn API client. -type Client struct { - username string - password string - - encoder *encoding.Encoder - - BaseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(username, password string) (*Client, error) { - if username == "" || password == "" { - return nil, errors.New("credentials missing") - } - - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - username: username, - password: password, - encoder: simplifiedchinese.GBK.NewEncoder(), - BaseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -// AddRecord adds a record. -// https://www.west.cn/CustomerCenter/doc/domain_v2.html#37u3001u6dfbu52a0u57dfu540du89e3u67900a3ca20id3d37u3001u6dfbu52a0u57dfu540du89e3u67903e203ca3e -func (c *Client) AddRecord(ctx context.Context, record Record) (int, error) { - values, err := querystring.Values(record) - if err != nil { - return 0, err - } - - req, err := c.newRequest(ctx, "domain", "adddnsrecord", values) - if err != nil { - return 0, err - } - - results := &APIResponse[RecordID]{} - - err = c.do(req, results) - if err != nil { - return 0, err - } - - if results.Result != http.StatusOK { - return 0, results - } - - return results.Data.ID, nil -} - -// DeleteRecord deleted a record. -// https://www.west.cn/CustomerCenter/doc/domain_v2.html#39u3001u5220u9664u57dfu540du89e3u67900a3ca20id3d39u3001u5220u9664u57dfu540du89e3u67903e203ca3e -func (c *Client) DeleteRecord(ctx context.Context, domain string, recordID int) error { - values := url.Values{} - values.Set("domain", domain) - values.Set("id", strconv.Itoa(recordID)) - - req, err := c.newRequest(ctx, "domain", "deldnsrecord", values) - if err != nil { - return err - } - - results := &APIResponse[any]{} - - err = c.do(req, results) - if err != nil { - return err - } - - if results.Result != http.StatusOK { - return results - } - - return nil -} - -func (c *Client) newRequest(ctx context.Context, p, act string, form url.Values) (*http.Request, error) { - if form == nil { - form = url.Values{} - } - - c.sign(form, time.Now()) - - values, err := c.convertURLValues(form) - if err != nil { - return nil, err - } - - endpoint := c.BaseURL.JoinPath(p, "/") - - query := endpoint.Query() - query.Set("act", act) - endpoint.RawQuery = query.Encode() - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), strings.NewReader(values.Encode())) - if err != nil { - return nil, err - } - - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - - return req, nil -} - -func (c *Client) sign(form url.Values, now time.Time) { - timestamp := strconv.FormatInt(now.UnixMilli(), 10) - - sum := md5.Sum([]byte(c.username + c.password + timestamp)) - - form.Set("token", hex.EncodeToString(sum[:])) - form.Set("username", c.username) - form.Set("time", timestamp) -} - -func (c *Client) do(req *http.Request, result any) error { - resp, err := c.HTTPClient.Do(req) - if err != nil { - return err - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = gbkDecoder(raw).Decode(result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func (c *Client) convertURLValues(values url.Values) (url.Values, error) { - results := make(url.Values) - - for key, vs := range values { - encKey, err := c.encoder.String(key) - if err != nil { - return nil, err - } - - for _, value := range vs { - encValue, err := c.encoder.String(value) - if err != nil { - return nil, err - } - - results.Add(encKey, encValue) - } - } - - return results, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - result := &APIResponse[any]{} - - err := gbkDecoder(raw).Decode(result) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return result -} - -func gbkDecoder(raw []byte) *json.Decoder { - return json.NewDecoder(transform.NewReader(bytes.NewBuffer(raw), simplifiedchinese.GBK.NewDecoder())) -} diff --git a/providers/dns/internal/westcn/internal/client_test.go b/providers/dns/internal/westcn/internal/client_test.go deleted file mode 100644 index 53fd6ed8f..000000000 --- a/providers/dns/internal/westcn/internal/client_test.go +++ /dev/null @@ -1,167 +0,0 @@ -package internal - -import ( - "net/http/httptest" - "net/url" - "testing" - "time" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/text/encoding/simplifiedchinese" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("user", "secret") - if err != nil { - return nil, err - } - - client.HTTPClient = server.Client() - client.BaseURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckHeader(). - WithContentTypeFromURLEncoded()) -} - -func TestClientAddRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /domain/", - servermock.ResponseFromFixture("adddnsrecord.json"). - WithHeader("Content-Type", "application/json", "Charset=gb2312"), - servermock.CheckQueryParameter().Strict(). - With("act", "adddnsrecord"), - servermock.CheckForm().UsePostForm().Strict(). - With("domain", "example.com"). - With("host", "@"). - With("ttl", "60"). - With("type", "TXT"). - With("value", "txtTXTtxt"). - // With("act", "adddnsrecord"). - With("username", "user"). - WithRegexp("time", `\d+`). - WithRegexp("token", `[a-z0-9]{32}`), - ). - Build(t) - - record := Record{ - Domain: "example.com", - Host: "@", - Type: "TXT", - Value: "txtTXTtxt", - TTL: 60, - } - - id, err := client.AddRecord(t.Context(), record) - require.NoError(t, err) - - assert.Equal(t, 123456, id) -} - -func TestClientAddRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /domain/", - servermock.ResponseFromFixture("error.json"). - WithHeader("Content-Type", "application/json", "Charset=gb2312"), - servermock.CheckQueryParameter().Strict(). - With("act", "adddnsrecord"), - ). - Build(t) - - record := Record{ - Domain: "example.com", - Host: "@", - Type: "TXT", - Value: "txtTXTtxt", - TTL: 60, - } - - _, err := client.AddRecord(t.Context(), record) - require.Error(t, err) - - require.EqualError(t, err, "10000: username,time,token必传 (500)") -} - -func TestClientDeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /domain/", - servermock.ResponseFromFixture("deldnsrecord.json"). - WithHeader("Content-Type", "application/json", "Charset=gb2312"), - servermock.CheckQueryParameter().Strict(). - With("act", "deldnsrecord"), - servermock.CheckForm().UsePostForm().Strict(). - With("id", "123"). - With("domain", "example.com"). - With("username", "user"). - WithRegexp("time", `\d+`). - WithRegexp("token", `[a-z0-9]{32}`), - ). - Build(t) - - err := client.DeleteRecord(t.Context(), "example.com", 123) - require.NoError(t, err) -} - -func TestClientDeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /domain/", - servermock.ResponseFromFixture("error.json"). - WithHeader("Content-Type", "application/json", "Charset=gb2312"), - servermock.CheckQueryParameter().Strict(). - With("act", "deldnsrecord"), - ). - Build(t) - err := client.DeleteRecord(t.Context(), "example.com", 123) - require.Error(t, err) - - require.EqualError(t, err, "10000: username,time,token必传 (500)") -} - -func Test_convertURLValues(t *testing.T) { - client, err := NewClient("user", "secret") - require.NoError(t, err) - - key := "你好abc" - value := "世界def" - - form := url.Values{} - form.Set(key, value) - - values, err := client.convertURLValues(form) - require.NoError(t, err) - - encoder := simplifiedchinese.GBK.NewEncoder() - - k, err := encoder.String(key) - require.NoError(t, err) - - v, err := encoder.String(value) - require.NoError(t, err) - - assert.Equal(t, v, values.Get(k)) - - decoder := simplifiedchinese.GBK.NewDecoder() - - decValue, err := decoder.String(values.Get(k)) - require.NoError(t, err) - - assert.Equal(t, value, decValue) -} - -func TestClient_sign(t *testing.T) { - client, err := NewClient("zhangsan", "5dh232kfg!*") - require.NoError(t, err) - - form := url.Values{} - - client.sign(form, time.UnixMilli(1554691950854)) - - assert.Equal(t, "zhangsan", form.Get("username")) - assert.Equal(t, "1554691950854", form.Get("time")) - assert.Equal(t, "f17581fb2535b2a7ee4468eb3f96a2a9", form.Get("token")) -} diff --git a/providers/dns/internal/westcn/internal/fixtures/adddnsrecord.json b/providers/dns/internal/westcn/internal/fixtures/adddnsrecord.json deleted file mode 100644 index f1c135206..000000000 --- a/providers/dns/internal/westcn/internal/fixtures/adddnsrecord.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "result": 200, - "clientid": "54880064508339547956", - "data": { - "id": 123456 - } -} diff --git a/providers/dns/internal/westcn/internal/fixtures/deldnsrecord.json b/providers/dns/internal/westcn/internal/fixtures/deldnsrecord.json deleted file mode 100644 index e97e92f74..000000000 --- a/providers/dns/internal/westcn/internal/fixtures/deldnsrecord.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "result": 200, - "clientid": "54880064508339547956" -} diff --git a/providers/dns/internal/westcn/internal/fixtures/error.json b/providers/dns/internal/westcn/internal/fixtures/error.json deleted file mode 100644 index 1c92415de..000000000 --- a/providers/dns/internal/westcn/internal/fixtures/error.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "result": 500, - "clientid": "54880064508339547956", - "msg": "username,time,tokenش", - "errcode": 10000 -} diff --git a/providers/dns/internal/westcn/internal/types.go b/providers/dns/internal/westcn/internal/types.go deleted file mode 100644 index d8d66be2c..000000000 --- a/providers/dns/internal/westcn/internal/types.go +++ /dev/null @@ -1,28 +0,0 @@ -package internal - -import "fmt" - -type APIResponse[T any] struct { - Result int `json:"result,omitempty"` - ClientID string `json:"clientid,omitempty"` - Message string `json:"msg,omitempty"` - ErrorCode int `json:"errcode,omitempty"` - Data T `json:"data,omitempty"` -} - -func (a APIResponse[T]) Error() string { - return fmt.Sprintf("%d: %s (%d)", a.ErrorCode, a.Message, a.Result) -} - -type Record struct { - Domain string `url:"domain,omitempty"` - Host string `url:"host,omitempty"` - Type string `url:"type,omitempty"` - Value string `url:"value,omitempty"` - TTL int `url:"ttl,omitempty"` // 60~86400 seconds - Priority int `url:"level,omitempty"` -} - -type RecordID struct { - ID int `json:"id,omitempty"` -} diff --git a/providers/dns/internal/westcn/provider.go b/providers/dns/internal/westcn/provider.go deleted file mode 100644 index a9e6dad58..000000000 --- a/providers/dns/internal/westcn/provider.go +++ /dev/null @@ -1,140 +0,0 @@ -package westcn - -import ( - "context" - "errors" - "fmt" - "net/http" - "net/url" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/internal/westcn/internal" -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - Username string - Password string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client - - recordIDs map[string]int - recordIDsMu sync.Mutex -} - -// NewDNSProviderConfig return a DNSProvider instance configured for West.cn/西部数码. -func NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.Username, config.Password) - if err != nil { - return nil, fmt.Errorf("%w", err) - } - - if baseURL != "" { - client.BaseURL, err = url.Parse(baseURL) - if err != nil { - return nil, err - } - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - recordIDs: make(map[string]int), - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("%w", err) - } - - record := internal.Record{ - Domain: dns01.UnFqdn(authZone), - Host: subDomain, - Type: "TXT", - Value: info.Value, - TTL: d.config.TTL, - } - - recordID, err := d.client.AddRecord(context.Background(), record) - if err != nil { - return fmt.Errorf("add record: %w", err) - } - - d.recordIDsMu.Lock() - d.recordIDs[token] = recordID - d.recordIDsMu.Unlock() - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("could not find zone for domain %q: %w", domain, err) - } - - // gets the record's unique ID - d.recordIDsMu.Lock() - recordID, ok := d.recordIDs[token] - d.recordIDsMu.Unlock() - - if !ok { - return fmt.Errorf("unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) - } - - err = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), recordID) - if err != nil { - return fmt.Errorf("delete record: %w", err) - } - - // deletes record ID from map - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/internal/westcn/provider_test.go b/providers/dns/internal/westcn/provider_test.go deleted file mode 100644 index 2ae0f09cb..000000000 --- a/providers/dns/internal/westcn/provider_test.go +++ /dev/null @@ -1,127 +0,0 @@ -package westcn - -import ( - "net/http/httptest" - "testing" - "time" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - username string - password string - expected string - }{ - { - desc: "success", - username: "user", - password: "secret", - }, - { - desc: "missing username", - password: "secret", - expected: "credentials missing", - }, - { - desc: "missing password", - username: "user", - expected: "credentials missing", - }, - { - desc: "missing credentials", - expected: "credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := &Config{} - config.Username = test.username - config.Password = test.password - - p, err := NewDNSProviderConfig(config, "") - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := &Config{ - Username: "user", - Password: "secret", - PropagationTimeout: 10 * time.Second, - PollingInterval: 1 * time.Second, - TTL: 120, - HTTPClient: server.Client(), - } - - p, err := NewDNSProviderConfig(config, server.URL) - if err != nil { - return nil, err - } - - return p, nil - }, - servermock.CheckHeader(). - WithContentTypeFromURLEncoded()) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("POST /domain/", - servermock.ResponseFromInternal("adddnsrecord.json"). - WithHeader("Content-Type", "application/json", "Charset=gb2312"), - servermock.CheckQueryParameter().Strict(). - With("act", "adddnsrecord"), - servermock.CheckForm().UsePostForm().Strict(). - With("domain", "example.com"). - With("host", "_acme-challenge"). - With("ttl", "120"). - With("type", "TXT"). - With("value", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"). - // With("act", "adddnsrecord"). - With("username", "user"). - WithRegexp("time", `\d+`). - WithRegexp("token", `[a-z0-9]{32}`), - ). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("POST /domain/", - servermock.ResponseFromInternal("deldnsrecord.json"). - WithHeader("Content-Type", "application/json", "Charset=gb2312"), - servermock.CheckQueryParameter().Strict(). - With("act", "deldnsrecord"), - servermock.CheckForm().UsePostForm().Strict(). - With("id", "123"). - With("domain", "example.com"). - With("username", "user"). - WithRegexp("time", `\d+`). - WithRegexp("token", `[a-z0-9]{32}`), - ). - Build(t) - - provider.recordIDs["abc"] = 123 - - err := provider.CleanUp("example.com", "abc", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/internetbs/internal/client.go b/providers/dns/internetbs/internal/client.go index cf9e90dc5..771408c5d 100644 --- a/providers/dns/internetbs/internal/client.go +++ b/providers/dns/internetbs/internal/client.go @@ -34,7 +34,7 @@ type Client struct { } // NewClient creates a new Client. -func NewClient(apiKey, password string) *Client { +func NewClient(apiKey string, password string) *Client { baseURL, _ := url.Parse(baseURL) return &Client{ @@ -46,9 +46,8 @@ func NewClient(apiKey, password string) *Client { } // AddRecord The command is intended to add a new DNS record to a specific zone (domain). -func (c *Client) AddRecord(ctx context.Context, query RecordQuery) error { +func (c Client) AddRecord(ctx context.Context, query RecordQuery) error { var r APIResponse - err := c.doRequest(ctx, "Add", query, &r) if err != nil { return err @@ -62,9 +61,8 @@ func (c *Client) AddRecord(ctx context.Context, query RecordQuery) error { } // RemoveRecord The command is intended to remove a DNS record from a specific zone. -func (c *Client) RemoveRecord(ctx context.Context, query RecordQuery) error { +func (c Client) RemoveRecord(ctx context.Context, query RecordQuery) error { var r APIResponse - err := c.doRequest(ctx, "Remove", query, &r) if err != nil { return err @@ -78,9 +76,8 @@ func (c *Client) RemoveRecord(ctx context.Context, query RecordQuery) error { } // ListRecords The command is intended to retrieve the list of DNS records for a specific domain. -func (c *Client) ListRecords(ctx context.Context, query ListRecordQuery) ([]Record, error) { +func (c Client) ListRecords(ctx context.Context, query ListRecordQuery) ([]Record, error) { var l ListResponse - err := c.doRequest(ctx, "List", query, &l) if err != nil { return nil, err @@ -93,7 +90,7 @@ func (c *Client) ListRecords(ctx context.Context, query ListRecordQuery) ([]Reco return l.Records, nil } -func (c *Client) doRequest(ctx context.Context, action string, params, result any) error { +func (c Client) doRequest(ctx context.Context, action string, params any, result any) error { endpoint := c.baseURL.JoinPath("Domain", "DnsRecord", action) values, err := querystring.Values(params) diff --git a/providers/dns/internetbs/internal/client_test.go b/providers/dns/internetbs/internal/client_test.go index 4532426d5..a22f1b121 100644 --- a/providers/dns/internetbs/internal/client_test.go +++ b/providers/dns/internetbs/internal/client_test.go @@ -1,14 +1,16 @@ package internal import ( + "context" "fmt" + "io" + "net/http" "net/http/httptest" "net/url" "os" "strconv" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -20,33 +22,8 @@ const ( testPassword = "testpass" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient(testAPIKey, testPassword) - client.baseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader(). - WithContentTypeFromURLEncoded(), - ) -} - func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /Domain/DnsRecord/Add", - servermock.ResponseFromFixture("Domain_DnsRecord_Add_SUCCESS.json"), - servermock.CheckForm().Strict(). - With("fullrecordname", "www.example.com"). - With("ttl", "36000"). - With("type", "TXT"). - With("value", "xxx"). - With("password", testPassword). - With("apiKey", testAPIKey). - With("ResponseFormat", "JSON")). - Build(t) + client := setupTest(t, "/Domain/DnsRecord/Add", "./fixtures/Domain_DnsRecord_Add_SUCCESS.json") query := RecordQuery{ FullRecordName: "www.example.com", @@ -55,15 +32,12 @@ func TestClient_AddRecord(t *testing.T) { TTL: 36000, } - err := client.AddRecord(t.Context(), query) + err := client.AddRecord(context.Background(), query) require.NoError(t, err) } func TestClient_AddRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /Domain/DnsRecord/Add", - servermock.ResponseFromFixture("Domain_DnsRecord_Add_FAILURE.json")). - Build(t) + client := setupTest(t, "/Domain/DnsRecord/Add", "./fixtures/Domain_DnsRecord_Add_FAILURE.json") query := RecordQuery{ FullRecordName: "www.example.com.", @@ -72,7 +46,7 @@ func TestClient_AddRecord_error(t *testing.T) { TTL: 36000, } - err := client.AddRecord(t.Context(), query) + err := client.AddRecord(context.Background(), query) require.Error(t, err) } @@ -93,7 +67,7 @@ func TestClient_AddRecord_integration(t *testing.T) { TTL: 36000, } - err := client.AddRecord(t.Context(), query) + err := client.AddRecord(context.Background(), query) require.NoError(t, err) query = RecordQuery{ @@ -103,43 +77,31 @@ func TestClient_AddRecord_integration(t *testing.T) { TTL: 36000, } - err = client.AddRecord(t.Context(), query) + err = client.AddRecord(context.Background(), query) require.NoError(t, err) } func TestClient_RemoveRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /Domain/DnsRecord/Remove", - servermock.ResponseFromFixture("Domain_DnsRecord_Remove_SUCCESS.json"), - servermock.CheckForm().Strict(). - With("fullrecordname", "www.example.com"). - With("type", "TXT"). - With("password", testPassword). - With("apiKey", testAPIKey). - With("ResponseFormat", "JSON")). - Build(t) + client := setupTest(t, "/Domain/DnsRecord/Remove", "./fixtures/Domain_DnsRecord_Remove_SUCCESS.json") query := RecordQuery{ FullRecordName: "www.example.com", Type: "TXT", Value: "", } - err := client.RemoveRecord(t.Context(), query) + err := client.RemoveRecord(context.Background(), query) require.NoError(t, err) } func TestClient_RemoveRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /Domain/DnsRecord/Remove", - servermock.ResponseFromFixture("Domain_DnsRecord_Remove_FAILURE.json")). - Build(t) + client := setupTest(t, "/Domain/DnsRecord/Remove", "./fixtures/Domain_DnsRecord_Remove_FAILURE.json") query := RecordQuery{ FullRecordName: "www.example.com.", Type: "TXT", Value: "", } - err := client.RemoveRecord(t.Context(), query) + err := client.RemoveRecord(context.Background(), query) require.Error(t, err) } @@ -159,26 +121,18 @@ func TestClient_RemoveRecord_integration(t *testing.T) { Value: "", } - err := client.RemoveRecord(t.Context(), query) + err := client.RemoveRecord(context.Background(), query) require.NoError(t, err) } func TestClient_ListRecords(t *testing.T) { - client := mockBuilder(). - Route("POST /Domain/DnsRecord/List", - servermock.ResponseFromFixture("Domain_DnsRecord_List_SUCCESS.json"), - servermock.CheckForm().Strict(). - With("Domain", "example.com"). - With("password", testPassword). - With("apiKey", testAPIKey). - With("ResponseFormat", "JSON")). - Build(t) + client := setupTest(t, "/Domain/DnsRecord/List", "./fixtures/Domain_DnsRecord_List_SUCCESS.json") query := ListRecordQuery{ Domain: "example.com", } - records, err := client.ListRecords(t.Context(), query) + records, err := client.ListRecords(context.Background(), query) require.NoError(t, err) expected := []Record{ @@ -224,16 +178,13 @@ func TestClient_ListRecords(t *testing.T) { } func TestClient_ListRecords_error(t *testing.T) { - client := mockBuilder(). - Route("POST /Domain/DnsRecord/List", - servermock.ResponseFromFixture("Domain_DnsRecord_List_FAILURE.json")). - Build(t) + client := setupTest(t, "/Domain/DnsRecord/List", "./fixtures/Domain_DnsRecord_List_FAILURE.json") query := ListRecordQuery{ Domain: "www.example.com", } - _, err := client.ListRecords(t.Context(), query) + _, err := client.ListRecords(context.Background(), query) require.Error(t, err) } @@ -251,10 +202,58 @@ func TestClient_ListRecords_integration(t *testing.T) { Domain: "example.com", } - records, err := client.ListRecords(t.Context(), query) + records, err := client.ListRecords(context.Background(), query) require.NoError(t, err) for _, record := range records { fmt.Println(record) } } + +func setupTest(t *testing.T, path, filename string) *Client { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc(path, testHandler(filename)) + + client := NewClient(testAPIKey, testPassword) + client.baseURL, _ = url.Parse(server.URL) + + return client +} + +func testHandler(filename string) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + if req.FormValue("apiKey") != testAPIKey { + http.Error(rw, `{"transactid":"d46d812569acdb8b39c3933ec4351e79","status":"FAILURE","message":"Invalid API key and\/or Password","code":107002}`, http.StatusOK) + return + } + + if req.FormValue("password") != testPassword { + http.Error(rw, `{"transactid":"d46d812569acdb8b39c3933ec4351e79","status":"FAILURE","message":"Invalid API key and\/or Password","code":107002}`, http.StatusOK) + return + } + + file, err := os.Open(filename) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + } +} diff --git a/providers/dns/internetbs/internal/fixtures/auth_error.json b/providers/dns/internetbs/internal/fixtures/auth_error.json deleted file mode 100644 index a40a0ef5e..000000000 --- a/providers/dns/internetbs/internal/fixtures/auth_error.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "transactid": "d46d812569acdb8b39c3933ec4351e79", - "status": "FAILURE", - "message": "Invalid API key and\/or Password", - "code": 107002 -} diff --git a/providers/dns/internetbs/internetbs.go b/providers/dns/internetbs/internetbs.go index e8cb868d2..89b33eae3 100644 --- a/providers/dns/internetbs/internetbs.go +++ b/providers/dns/internetbs/internetbs.go @@ -8,10 +8,8 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/internetbs/internal" ) @@ -28,8 +26,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string @@ -89,8 +85,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, client: client, diff --git a/providers/dns/internetbs/internetbs.toml b/providers/dns/internetbs/internetbs.toml index f22850253..6f705ba62 100644 --- a/providers/dns/internetbs/internetbs.toml +++ b/providers/dns/internetbs/internetbs.toml @@ -7,7 +7,7 @@ Since = "v4.5.0" Example = ''' INTERNET_BS_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx \ INTERNET_BS_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \ -lego --dns internetbs -d '*.example.com' -d example.com run +lego --email you@example.com --dns internetbs --domains my.example.org run ''' [Configuration] @@ -15,10 +15,10 @@ lego --dns internetbs -d '*.example.com' -d example.com run INTERNET_BS_API_KEY = "API key" INTERNET_BS_PASSWORD = "API password" [Configuration.Additional] - INTERNET_BS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - INTERNET_BS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - INTERNET_BS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)" - INTERNET_BS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + INTERNET_BS_POLLING_INTERVAL = "Time between DNS propagation check" + INTERNET_BS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + INTERNET_BS_TTL = "The TTL of the TXT record used for the DNS challenge" + INTERNET_BS_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://internetbs.net/internet-bs-api.pdf" diff --git a/providers/dns/internetbs/internetbs_test.go b/providers/dns/internetbs/internetbs_test.go index be436d6e7..ea328d506 100644 --- a/providers/dns/internetbs/internetbs_test.go +++ b/providers/dns/internetbs/internetbs_test.go @@ -49,7 +49,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -122,7 +121,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -136,7 +134,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/inwx/inwx.go b/providers/dns/inwx/inwx.go index 0e79d71e0..dc26362f9 100644 --- a/providers/dns/inwx/inwx.go +++ b/providers/dns/inwx/inwx.go @@ -6,7 +6,6 @@ import ( "fmt" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" @@ -28,8 +27,6 @@ const ( EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Username string @@ -46,7 +43,7 @@ func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 300), // INWX has rather unstable propagation delays, thus using a larger default value - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 6*time.Minute), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 360*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), Sandbox: env.GetOrDefaultBool(EnvSandbox, false), } @@ -97,14 +94,14 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) + challengeInfo := dns01.GetChallengeInfo(domain, keyAuth) - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + authZone, err := dns01.FindZoneByFqdn(challengeInfo.EffectiveFQDN) if err != nil { - return fmt.Errorf("inwx: could not find zone for domain %q (%s): %w", domain, info.EffectiveFQDN, err) + return fmt.Errorf("inwx: could not find zone for domain %q (%s): %w", domain, challengeInfo.EffectiveFQDN, err) } - login, err := d.client.Account.Login() + info, err := d.client.Account.Login() if err != nil { return fmt.Errorf("inwx: %w", err) } @@ -116,24 +113,27 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { } }() - err = d.twoFactorAuth(login) + err = d.twoFactorAuth(info) if err != nil { return fmt.Errorf("inwx: %w", err) } request := &goinwx.NameserverRecordRequest{ Domain: dns01.UnFqdn(authZone), - Name: dns01.UnFqdn(info.EffectiveFQDN), + Name: dns01.UnFqdn(challengeInfo.EffectiveFQDN), Type: "TXT", - Content: info.Value, + Content: challengeInfo.Value, TTL: d.config.TTL, } _, err = d.client.Nameservers.CreateRecord(request) if err != nil { var er *goinwx.ErrorResponse - if errors.As(err, &er) && er.Message == "Object exists" { - return nil + if errors.As(err, &er) { + if er.Message == "Object exists" { + return nil + } + return fmt.Errorf("inwx: %w", err) } return fmt.Errorf("inwx: %w", err) @@ -144,14 +144,14 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) + challengeInfo := dns01.GetChallengeInfo(domain, keyAuth) - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + authZone, err := dns01.FindZoneByFqdn(challengeInfo.EffectiveFQDN) if err != nil { - return fmt.Errorf("inwx: could not find zone for domain %q (%s): %w", domain, info.EffectiveFQDN, err) + return fmt.Errorf("inwx: could not find zone for domain %q (%s): %w", domain, challengeInfo.EffectiveFQDN, err) } - login, err := d.client.Account.Login() + info, err := d.client.Account.Login() if err != nil { return fmt.Errorf("inwx: %w", err) } @@ -163,42 +163,29 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } }() - err = d.twoFactorAuth(login) + err = d.twoFactorAuth(info) if err != nil { return fmt.Errorf("inwx: %w", err) } response, err := d.client.Nameservers.Info(&goinwx.NameserverInfoRequest{ Domain: dns01.UnFqdn(authZone), - Name: dns01.UnFqdn(info.EffectiveFQDN), + Name: dns01.UnFqdn(challengeInfo.EffectiveFQDN), Type: "TXT", }) if err != nil { return fmt.Errorf("inwx: %w", err) } - var recordID string - + var lastErr error for _, record := range response.Records { - if record.Content != info.Value { - continue + err = d.client.Nameservers.DeleteRecord(record.ID) + if err != nil { + lastErr = fmt.Errorf("inwx: %w", err) } - - recordID = record.ID - - break } - if recordID == "" { - return errors.New("inwx: TXT record not found") - } - - err = d.client.Nameservers.DeleteRecord(recordID) - if err != nil { - return fmt.Errorf("inwx: %w", err) - } - - return nil + return lastErr } // Timeout returns the timeout and interval to use when checking for DNS propagation. diff --git a/providers/dns/inwx/inwx.toml b/providers/dns/inwx/inwx.toml index da4c6d959..7e30463ae 100644 --- a/providers/dns/inwx/inwx.toml +++ b/providers/dns/inwx/inwx.toml @@ -7,13 +7,13 @@ Since = "v2.0.0" Example = ''' INWX_USERNAME=xxxxxxxxxx \ INWX_PASSWORD=yyyyyyyyyy \ -lego --dns inwx -d '*.example.com' -d example.com run +lego --email you@example.com --dns inwx --domains my.example.org run # 2FA INWX_USERNAME=xxxxxxxxxx \ INWX_PASSWORD=yyyyyyyyyy \ INWX_SHARED_SECRET=zzzzzzzzzz \ -lego --dns inwx -d '*.example.com' -d example.com run +lego --email you@example.com --dns inwx --domains my.example.org run ''' [Configuration] @@ -22,9 +22,9 @@ lego --dns inwx -d '*.example.com' -d example.com run INWX_PASSWORD = "Password" [Configuration.Additional] INWX_SHARED_SECRET = "shared secret related to 2FA" - INWX_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - INWX_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 360)" - INWX_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" + INWX_POLLING_INTERVAL = "Time between DNS propagation check" + INWX_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation (default 360s)" + INWX_TTL = "The TTL of the TXT record used for the DNS challenge" INWX_SANDBOX = "Activate the sandbox (boolean)" [Links] diff --git a/providers/dns/inwx/inwx_test.go b/providers/dns/inwx/inwx_test.go index 47b12e228..39ce7d70e 100644 --- a/providers/dns/inwx/inwx_test.go +++ b/providers/dns/inwx/inwx_test.go @@ -62,7 +62,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -125,7 +124,6 @@ func TestLivePresentAndCleanup(t *testing.T) { } envTest.RestoreEnv() - envTest.Apply(map[string]string{ EnvSandbox: "true", EnvTTL: "3600", // In sandbox mode, the minimum allowed TTL is 3600 diff --git a/providers/dns/internal/ionos/internal/client.go b/providers/dns/ionos/internal/client.go similarity index 97% rename from providers/dns/internal/ionos/internal/client.go rename to providers/dns/ionos/internal/client.go index 2a556a49b..8b37d5f1c 100644 --- a/providers/dns/internal/ionos/internal/client.go +++ b/providers/dns/ionos/internal/client.go @@ -14,11 +14,9 @@ import ( querystring "github.com/google/go-querystring/query" ) +// defaultBaseURL represents the API endpoint to call. const defaultBaseURL = "https://api.hosting.ionos.com/dns" -// APIKeyHeader API key header. -const APIKeyHeader = "X-Api-Key" - // Client Ionos API client. type Client struct { apiKey string @@ -51,7 +49,6 @@ func (c *Client) ListZones(ctx context.Context) ([]Zone, error) { } var zones []Zone - err = c.do(req, &zones) if err != nil { return nil, fmt.Errorf("failed to call API: %w", err) @@ -96,7 +93,6 @@ func (c *Client) GetRecords(ctx context.Context, zoneID string, filter *RecordsF } var zone CustomerZone - err = c.do(req, &zone) if err != nil { return nil, fmt.Errorf("failed to call API: %w", err) @@ -123,7 +119,7 @@ func (c *Client) RemoveRecord(ctx context.Context, zoneID, recordID string) erro } func (c *Client) do(req *http.Request, result any) error { - req.Header.Set(APIKeyHeader, c.apiKey) + req.Header.Set("X-API-Key", c.apiKey) resp, err := c.HTTPClient.Do(req) if err != nil { @@ -181,7 +177,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) errClient := &ClientError{StatusCode: resp.StatusCode} - err := json.Unmarshal(raw, &errClient.errors) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/ionos/internal/client_test.go b/providers/dns/ionos/internal/client_test.go new file mode 100644 index 000000000..21a7a2675 --- /dev/null +++ b/providers/dns/ionos/internal/client_test.go @@ -0,0 +1,184 @@ +package internal + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestClient_ListZones(t *testing.T) { + client, mux := setupTest(t) + + mux.HandleFunc("/v1/zones", mockHandler(http.MethodGet, http.StatusOK, "list_zones.json")) + + zones, err := client.ListZones(context.Background()) + require.NoError(t, err) + + expected := []Zone{{ + ID: "11af3414-ebba-11e9-8df5-66fbe8a334b4", + Name: "test.com", + Type: "NATIVE", + }} + + assert.Equal(t, expected, zones) +} + +func TestClient_ListZones_error(t *testing.T) { + client, mux := setupTest(t) + + mux.HandleFunc("/v1/zones", mockHandler(http.MethodGet, http.StatusUnauthorized, "list_zones_error.json")) + + zones, err := client.ListZones(context.Background()) + require.Error(t, err) + + assert.Nil(t, zones) + + var cErr *ClientError + assert.ErrorAs(t, err, &cErr) + assert.Equal(t, http.StatusUnauthorized, cErr.StatusCode) +} + +func TestClient_GetRecords(t *testing.T) { + client, mux := setupTest(t) + + mux.HandleFunc("/v1/zones/azone01", mockHandler(http.MethodGet, http.StatusOK, "get_records.json")) + + records, err := client.GetRecords(context.Background(), "azone01", nil) + require.NoError(t, err) + + expected := []Record{{ + ID: "22af3414-abbe-9e11-5df5-66fbe8e334b4", + Name: "string", + Content: "string", + Type: "A", + }} + + assert.Equal(t, expected, records) +} + +func TestClient_GetRecords_error(t *testing.T) { + client, mux := setupTest(t) + + mux.HandleFunc("/v1/zones/azone01", mockHandler(http.MethodGet, http.StatusUnauthorized, "get_records_error.json")) + + records, err := client.GetRecords(context.Background(), "azone01", nil) + require.Error(t, err) + + assert.Nil(t, records) + + var cErr *ClientError + assert.ErrorAs(t, err, &cErr) + assert.Equal(t, http.StatusUnauthorized, cErr.StatusCode) +} + +func TestClient_RemoveRecord(t *testing.T) { + client, mux := setupTest(t) + + mux.HandleFunc("/v1/zones/azone01/records/arecord01", mockHandler(http.MethodDelete, http.StatusOK, "")) + + err := client.RemoveRecord(context.Background(), "azone01", "arecord01") + require.NoError(t, err) +} + +func TestClient_RemoveRecord_error(t *testing.T) { + client, mux := setupTest(t) + + mux.HandleFunc("/v1/zones/azone01/records/arecord01", mockHandler(http.MethodDelete, http.StatusInternalServerError, "remove_record_error.json")) + + err := client.RemoveRecord(context.Background(), "azone01", "arecord01") + require.Error(t, err) + + var cErr *ClientError + assert.ErrorAs(t, err, &cErr) + assert.Equal(t, http.StatusInternalServerError, cErr.StatusCode) +} + +func TestClient_ReplaceRecords(t *testing.T) { + client, mux := setupTest(t) + + mux.HandleFunc("/v1/zones/azone01", mockHandler(http.MethodPatch, http.StatusOK, "")) + + records := []Record{{ + ID: "22af3414-abbe-9e11-5df5-66fbe8e334b4", + Name: "string", + Content: "string", + Type: "A", + }} + + err := client.ReplaceRecords(context.Background(), "azone01", records) + require.NoError(t, err) +} + +func TestClient_ReplaceRecords_error(t *testing.T) { + client, mux := setupTest(t) + + mux.HandleFunc("/v1/zones/azone01", mockHandler(http.MethodPatch, http.StatusBadRequest, "replace_records_error.json")) + + records := []Record{{ + ID: "22af3414-abbe-9e11-5df5-66fbe8e334b4", + Name: "string", + Content: "string", + Type: "A", + }} + + err := client.ReplaceRecords(context.Background(), "azone01", records) + require.Error(t, err) + + var cErr *ClientError + assert.ErrorAs(t, err, &cErr) + assert.Equal(t, http.StatusBadRequest, cErr.StatusCode) +} + +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client, err := NewClient("secret") + require.NoError(t, err) + + client.BaseURL, _ = url.Parse(server.URL) + + return client, mux +} + +func mockHandler(method string, statusCode int, filename string) func(http.ResponseWriter, *http.Request) { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + if filename == "" { + rw.WriteHeader(statusCode) + return + } + + file, err := os.Open(filepath.FromSlash(path.Join("./fixtures", filename))) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + rw.WriteHeader(statusCode) + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + } +} diff --git a/providers/dns/internal/ionos/internal/fixtures/get_records.json b/providers/dns/ionos/internal/fixtures/get_records.json similarity index 100% rename from providers/dns/internal/ionos/internal/fixtures/get_records.json rename to providers/dns/ionos/internal/fixtures/get_records.json diff --git a/providers/dns/internal/ionos/internal/fixtures/get_records_error.json b/providers/dns/ionos/internal/fixtures/get_records_error.json similarity index 100% rename from providers/dns/internal/ionos/internal/fixtures/get_records_error.json rename to providers/dns/ionos/internal/fixtures/get_records_error.json diff --git a/providers/dns/internal/ionos/internal/fixtures/list_zones.json b/providers/dns/ionos/internal/fixtures/list_zones.json similarity index 100% rename from providers/dns/internal/ionos/internal/fixtures/list_zones.json rename to providers/dns/ionos/internal/fixtures/list_zones.json diff --git a/providers/dns/internal/ionos/internal/fixtures/list_zones_error.json b/providers/dns/ionos/internal/fixtures/list_zones_error.json similarity index 100% rename from providers/dns/internal/ionos/internal/fixtures/list_zones_error.json rename to providers/dns/ionos/internal/fixtures/list_zones_error.json diff --git a/providers/dns/internal/ionos/internal/fixtures/remove_record_error.json b/providers/dns/ionos/internal/fixtures/remove_record_error.json similarity index 100% rename from providers/dns/internal/ionos/internal/fixtures/remove_record_error.json rename to providers/dns/ionos/internal/fixtures/remove_record_error.json diff --git a/providers/dns/internal/ionos/internal/fixtures/replace_records_error.json b/providers/dns/ionos/internal/fixtures/replace_records_error.json similarity index 100% rename from providers/dns/internal/ionos/internal/fixtures/replace_records_error.json rename to providers/dns/ionos/internal/fixtures/replace_records_error.json diff --git a/providers/dns/internal/ionos/internal/types.go b/providers/dns/ionos/internal/types.go similarity index 91% rename from providers/dns/internal/ionos/internal/types.go rename to providers/dns/ionos/internal/types.go index 35bfe0966..3b7acbec2 100644 --- a/providers/dns/internal/ionos/internal/types.go +++ b/providers/dns/ionos/internal/types.go @@ -3,7 +3,6 @@ package internal import ( "fmt" "strconv" - "strings" ) // ClientError a detailed error. @@ -14,23 +13,21 @@ type ClientError struct { } func (f ClientError) Error() string { - var msg strings.Builder - - msg.WriteString(strconv.Itoa(f.StatusCode) + ": ") + msg := strconv.Itoa(f.StatusCode) + ": " if f.message != "" { - msg.WriteString(f.message + ": ") + msg += f.message + ": " } for i, e := range f.errors { if i != 0 { - msg.WriteString(", ") + msg += ", " } - msg.WriteString(e.Error()) + msg += e.Error() } - return msg.String() + return msg } func (f ClientError) Unwrap() error { diff --git a/providers/dns/ionos/ionos.go b/providers/dns/ionos/ionos.go index 892370f5d..6a778f2b2 100644 --- a/providers/dns/ionos/ionos.go +++ b/providers/dns/ionos/ionos.go @@ -2,17 +2,21 @@ package ionos import ( + "context" "errors" "fmt" "net/http" + "strconv" + "strings" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/ionos" + "github.com/go-acme/lego/v4/providers/dns/ionos/internal" ) +const minTTL = 300 + // Environment variables names. const ( envNamespace = "IONOS_" @@ -25,18 +29,20 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const minTTL = 300 - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. -type Config = ionos.Config +type Config struct { + APIKey string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, ionos.MinTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 15*time.Minute), + TTL: env.GetOrDefaultInt(EnvTTL, minTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), @@ -46,7 +52,8 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - prv challenge.ProviderTimeout + config *Config + client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for Ionos. @@ -69,36 +76,124 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("ionos: the configuration of the DNS provider is nil") } - provider, err := ionos.NewDNSProviderConfig(config, "") + if config.APIKey == "" { + return nil, errors.New("ionos: credentials missing") + } + + if config.TTL < minTTL { + return nil, fmt.Errorf("ionos: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) + } + + client, err := internal.NewClient(config.APIKey) if err != nil { return nil, fmt.Errorf("ionos: %w", err) } - return &DNSProvider{prv: provider}, nil + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + return &DNSProvider{config: config, client: client}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.prv.Timeout() + return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - err := d.prv.Present(domain, token, keyAuth) +func (d *DNSProvider) Present(domain, _, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + zones, err := d.client.ListZones(ctx) if err != nil { - return fmt.Errorf("ionos: %w", err) + return fmt.Errorf("ionos: failed to get zones: %w", err) + } + + // TODO(ldez) replace domain by FQDN to follow CNAME. + zone := findZone(zones, domain) + if zone == nil { + return errors.New("ionos: no matching zone found for domain") + } + + filter := &internal.RecordsFilter{ + Suffix: dns01.UnFqdn(info.EffectiveFQDN), + RecordType: "TXT", + } + + records, err := d.client.GetRecords(ctx, zone.ID, filter) + if err != nil { + return fmt.Errorf("ionos: failed to get records (zone=%s): %w", zone.ID, err) + } + + records = append(records, internal.Record{ + Name: dns01.UnFqdn(info.EffectiveFQDN), + Content: info.Value, + TTL: d.config.TTL, + Type: "TXT", + }) + + err = d.client.ReplaceRecords(ctx, zone.ID, records) + if err != nil { + return fmt.Errorf("ionos: failed to create/update records (zone=%s): %w", zone.ID, err) } return nil } // CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - err := d.prv.CleanUp(domain, token, keyAuth) +func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + zones, err := d.client.ListZones(ctx) if err != nil { - return fmt.Errorf("ionos: %w", err) + return fmt.Errorf("ionos: failed to get zones: %w", err) } - return nil + // TODO(ldez) replace domain by FQDN to follow CNAME. + zone := findZone(zones, domain) + if zone == nil { + return errors.New("ionos: no matching zone found for domain") + } + + filter := &internal.RecordsFilter{ + Suffix: dns01.UnFqdn(info.EffectiveFQDN), + RecordType: "TXT", + } + + records, err := d.client.GetRecords(ctx, zone.ID, filter) + if err != nil { + return fmt.Errorf("ionos: failed to get records (zone=%s): %w", zone.ID, err) + } + + for _, record := range records { + if record.Name == dns01.UnFqdn(info.EffectiveFQDN) && record.Content == strconv.Quote(info.Value) { + err = d.client.RemoveRecord(ctx, zone.ID, record.ID) + if err != nil { + return fmt.Errorf("ionos: failed to remove record (zone=%s, record=%s): %w", zone.ID, record.ID, err) + } + return nil + } + } + + return fmt.Errorf("ionos: failed to remove record, record not found (zone=%s, domain=%s, fqdn=%s, value=%s)", zone.ID, domain, info.EffectiveFQDN, info.Value) +} + +func findZone(zones []internal.Zone, domain string) *internal.Zone { + var result *internal.Zone + + for _, zone := range zones { + if zone.Name != "" && strings.HasSuffix(domain, zone.Name) { + if result == nil || len(zone.Name) > len(result.Name) { + result = &zone + } + } + } + + return result } diff --git a/providers/dns/ionos/ionos.toml b/providers/dns/ionos/ionos.toml index a2c9518fb..b545128e6 100644 --- a/providers/dns/ionos/ionos.toml +++ b/providers/dns/ionos/ionos.toml @@ -6,17 +6,17 @@ Since = "v4.2.0" Example = ''' IONOS_API_KEY=xxxxxxxx \ -lego --dns ionos -d '*.example.com' -d example.com run +lego --email you@example.com --dns ionos --domains my.example.org run ''' [Configuration] [Configuration.Credentials] IONOS_API_KEY = "API key `.` https://developer.hosting.ionos.com/docs/getstarted" [Configuration.Additional] - IONOS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - IONOS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 900)" - IONOS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - IONOS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + IONOS_POLLING_INTERVAL = "Time between DNS propagation check" + IONOS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + IONOS_TTL = "The TTL of the TXT record used for the DNS challenge" + IONOS_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://developer.hosting.ionos.com/docs/dns" diff --git a/providers/dns/ionos/ionos_test.go b/providers/dns/ionos/ionos_test.go index 39dc0c511..5aef6ad14 100644 --- a/providers/dns/ionos/ionos_test.go +++ b/providers/dns/ionos/ionos_test.go @@ -9,7 +9,9 @@ import ( const envDomain = envNamespace + "DOMAIN" -var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) +var envTest = tester.NewEnvTest( + EnvAPIKey). + WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { @@ -35,7 +37,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -45,7 +46,8 @@ func TestNewDNSProvider(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - require.NotNil(t, p.prv) + require.NotNil(t, p.config) + require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } @@ -89,7 +91,8 @@ func TestNewDNSProviderConfig(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - require.NotNil(t, p.prv) + require.NotNil(t, p.config) + require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } @@ -103,7 +106,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -117,7 +119,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/ionoscloud/internal/client.go b/providers/dns/ionoscloud/internal/client.go deleted file mode 100644 index 5b7d3a0fc..000000000 --- a/providers/dns/ionoscloud/internal/client.go +++ /dev/null @@ -1,172 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" -) - -const defaultBaseURL = "https://dns.de-fra.ionos.com" - -const authorizationHeader = "Authorization" - -// Client the Ionos Cloud API client. -type Client struct { - apiKey string - - BaseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(apiKey string) (*Client, error) { - if apiKey == "" { - return nil, errors.New("credentials missing") - } - - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - apiKey: apiKey, - BaseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -// RetrieveZones returns a list of the DNS zones. -// https://api.ionos.com/docs/dns/v1/#tag/Zones/operation/zonesGet -func (c *Client) RetrieveZones(ctx context.Context, zoneName string) ([]Zone, error) { - endpoint := c.BaseURL.JoinPath("zones") - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - query := req.URL.Query() - query.Add("filter.zoneName", zoneName) - req.URL.RawQuery = query.Encode() - - result := ZonesResponse{} - - if err := c.do(req, &result); err != nil { - return nil, err - } - - return result.Items, nil -} - -// CreateRecord creates a new record for the DNS zone. -// https://api.ionos.com/docs/dns/v1/#tag/Records/operation/zonesRecordsPost -func (c *Client) CreateRecord(ctx context.Context, zoneID string, record RecordProperties) (*RecordResponse, error) { - endpoint := c.BaseURL.JoinPath("zones", zoneID, "records") - - payload := map[string]RecordProperties{ - "properties": record, - } - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, payload) - if err != nil { - return nil, err - } - - result := &RecordResponse{} - - if err := c.do(req, result); err != nil { - return nil, err - } - - return result, nil -} - -// DeleteRecord deletes a specified record from the DNS zone. -// https://api.ionos.com/docs/dns/v1/#tag/Records/operation/zonesRecordsDelete -func (c *Client) DeleteRecord(ctx context.Context, zoneID, recordID string) error { - endpoint := c.BaseURL.JoinPath("zones", zoneID, "records", recordID) - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return err - } - - return c.do(req, nil) -} - -func (c *Client) do(req *http.Request, result any) error { - useragent.SetHeader(req.Header) - - req.Header.Set(authorizationHeader, "Bearer "+c.apiKey) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIError - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return &errAPI -} diff --git a/providers/dns/ionoscloud/internal/client_test.go b/providers/dns/ionoscloud/internal/client_test.go deleted file mode 100644 index dc478cc64..000000000 --- a/providers/dns/ionoscloud/internal/client_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - "time" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("secret") - if err != nil { - return nil, err - } - - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(). - WithAuthorization("Bearer secret"), - ) -} - -func TestClient_RetrieveZones(t *testing.T) { - client := mockBuilder(). - Route("GET /zones", - servermock.ResponseFromFixture("zones.json"), - servermock.CheckQueryParameter().Strict(). - With("filter.zoneName", "example.com")). - Build(t) - - zones, err := client.RetrieveZones(t.Context(), "example.com") - require.NoError(t, err) - - expected := []Zone{{ - ID: "e74d0d15-f567-4b7b-9069-26ee1f93bae3", - Type: "zone", - Metadata: ZoneMetadata{ - CreatedDate: time.Date(2022, time.August, 21, 15, 52, 53, 0, time.UTC), - CreatedBy: "ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3", - CreatedByUserID: "87f9a82e-b28d-49ed-9d04-fba2c0459cd3", - LastModifiedDate: time.Date(2022, time.August, 21, 15, 52, 53, 0, time.UTC), - LastModifiedBy: "ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3", - LastModifiedByUserID: "63cef532-26fe-4a64-a4e0-de7c8a506c90", - ResourceURN: "ionos::::", - State: "PROVISIONING", - Nameservers: []string{"ns-ic.ui-dns.com", "ns-ic.ui-dns.de", "ns-ic.ui-dns.org", "ns-ic.ui-dns.biz"}, - }, - Properties: ZoneProperties{ - ZoneName: "example.com", - Description: "The hosted zone is used for example.com", - Enabled: true, - }, - }} - - assert.Equal(t, expected, zones) -} - -func TestClient_RetrieveZones_error(t *testing.T) { - client := mockBuilder(). - Route("GET /zones", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) - - _, err := client.RetrieveZones(t.Context(), "example.com") - require.EqualError(t, err, "401: paas-auth-1: Unauthorized, wrong or no api key provided to process this request") -} - -func TestClient_CreateRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /zones/abc/records", - servermock.ResponseFromFixture("create_record.json"), - servermock.CheckRequestJSONBodyFromFixture("create_record-request.json")). - Build(t) - - record := RecordProperties{ - Name: "_acme-challenge", - Type: "TXT", - Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: 120, - } - - result, err := client.CreateRecord(t.Context(), "abc", record) - require.NoError(t, err) - - expected := &RecordResponse{ - ID: "90d81ac0-3a30-44d4-95a5-12959effa6ee", - Type: "record", - Metadata: RecordMetadata{ - CreatedDate: time.Date(2022, time.August, 21, 15, 52, 53, 0, time.UTC), - CreatedBy: "ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3", - CreatedByUserID: "87f9a82e-b28d-49ed-9d04-fba2c0459cd3", - LastModifiedDate: time.Date(2022, time.August, 21, 15, 52, 53, 0, time.UTC), - LastModifiedBy: "ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3", - LastModifiedByUserID: "63cef532-26fe-4a64-a4e0-de7c8a506c90", - ResourceURN: "ionos::::", - State: "PROVISIONING", - Fqdn: "app.example.com", - ZoneID: "a363f30c-4c0c-4552-9a07-298d87f219bf", - }, - Properties: RecordProperties{ - Name: "app", - Type: "A", - Content: "1.2.3.4", - TTL: 3600, - Priority: 3600, - Enabled: true, - }, - } - - assert.Equal(t, expected, result) -} - -func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /zones/abc/records/def", - servermock.Noop(). - WithStatusCode(http.StatusAccepted)). - Build(t) - - err := client.DeleteRecord(t.Context(), "abc", "def") - require.NoError(t, err) -} diff --git a/providers/dns/ionoscloud/internal/fixtures/create_record-request.json b/providers/dns/ionoscloud/internal/fixtures/create_record-request.json deleted file mode 100644 index d4f52bba8..000000000 --- a/providers/dns/ionoscloud/internal/fixtures/create_record-request.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "properties": { - "name": "_acme-challenge", - "type": "TXT", - "content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "ttl": 120 - } -} diff --git a/providers/dns/ionoscloud/internal/fixtures/create_record.json b/providers/dns/ionoscloud/internal/fixtures/create_record.json deleted file mode 100644 index d3094c3b2..000000000 --- a/providers/dns/ionoscloud/internal/fixtures/create_record.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "id": "90d81ac0-3a30-44d4-95a5-12959effa6ee", - "type": "record", - "href": "", - "metadata": { - "createdDate": "2022-08-21T15:52:53Z", - "createdBy": "ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3", - "createdByUserId": "87f9a82e-b28d-49ed-9d04-fba2c0459cd3", - "lastModifiedDate": "2022-08-21T15:52:53Z", - "lastModifiedBy": "ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3", - "lastModifiedByUserId": "63cef532-26fe-4a64-a4e0-de7c8a506c90", - "resourceURN": "ionos::::", - "state": "PROVISIONING", - "fqdn": "app.example.com", - "zoneId": "a363f30c-4c0c-4552-9a07-298d87f219bf" - }, - "properties": { - "name": "app", - "type": "A", - "content": "1.2.3.4", - "ttl": 3600, - "priority": 3600, - "enabled": true - } -} diff --git a/providers/dns/ionoscloud/internal/fixtures/error.json b/providers/dns/ionoscloud/internal/fixtures/error.json deleted file mode 100644 index bed0e5efb..000000000 --- a/providers/dns/ionoscloud/internal/fixtures/error.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "httpStatus": 401, - "messages": [ - { - "errorCode": "paas-auth-1", - "message": "Unauthorized, wrong or no api key provided to process this request" - } - ] -} diff --git a/providers/dns/ionoscloud/internal/fixtures/zones.json b/providers/dns/ionoscloud/internal/fixtures/zones.json deleted file mode 100644 index c9c2c62f9..000000000 --- a/providers/dns/ionoscloud/internal/fixtures/zones.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "id": "e74d0d15-f567-4b7b-9069-26ee1f93bae3", - "type": "collection", - "href": "", - "offset": 0, - "limit": 1000, - "_links": { - "prev": "http://PREVIOUS-PAGE-URI", - "self": "http://THIS-PAGE-URI", - "next": "http://NEXT-PAGE-URI" - }, - "items": [ - { - "id": "e74d0d15-f567-4b7b-9069-26ee1f93bae3", - "type": "zone", - "href": "", - "metadata": { - "createdDate": "2022-08-21T15:52:53Z", - "createdBy": "ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3", - "createdByUserId": "87f9a82e-b28d-49ed-9d04-fba2c0459cd3", - "lastModifiedDate": "2022-08-21T15:52:53Z", - "lastModifiedBy": "ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3", - "lastModifiedByUserId": "63cef532-26fe-4a64-a4e0-de7c8a506c90", - "resourceURN": "ionos::::", - "state": "PROVISIONING", - "nameservers": [ - "ns-ic.ui-dns.com", - "ns-ic.ui-dns.de", - "ns-ic.ui-dns.org", - "ns-ic.ui-dns.biz" - ] - }, - "properties": { - "zoneName": "example.com", - "description": "The hosted zone is used for example.com", - "enabled": true - } - } - ] -} diff --git a/providers/dns/ionoscloud/internal/types.go b/providers/dns/ionoscloud/internal/types.go deleted file mode 100644 index 49348f4d1..000000000 --- a/providers/dns/ionoscloud/internal/types.go +++ /dev/null @@ -1,97 +0,0 @@ -package internal - -import ( - "fmt" - "strconv" - "strings" - "time" -) - -type APIError struct { - HTTPStatus int `json:"httpStatus"` - Messages []ErrorMessage `json:"messages"` -} - -func (a *APIError) Error() string { - var msg strings.Builder - - msg.WriteString(strconv.Itoa(a.HTTPStatus)) - - for _, m := range a.Messages { - msg.WriteString(": ") - msg.WriteString(m.String()) - } - - return msg.String() -} - -type ErrorMessage struct { - ErrorCode string `json:"errorCode"` - Message string `json:"message"` -} - -func (e ErrorMessage) String() string { - return fmt.Sprintf("%s: %s", e.ErrorCode, e.Message) -} - -type ZonesResponse struct { - ID string `json:"id"` - Type string `json:"type"` - Offset int `json:"offset"` - Limit int `json:"limit"` - Items []Zone `json:"items"` -} - -type Zone struct { - ID string `json:"id"` - Type string `json:"type"` - Metadata ZoneMetadata `json:"metadata"` - Properties ZoneProperties `json:"properties"` -} - -type ZoneMetadata struct { - CreatedDate time.Time `json:"createdDate"` - CreatedBy string `json:"createdBy"` - CreatedByUserID string `json:"createdByUserId"` - LastModifiedDate time.Time `json:"lastModifiedDate"` - LastModifiedBy string `json:"lastModifiedBy"` - LastModifiedByUserID string `json:"lastModifiedByUserId"` - ResourceURN string `json:"resourceURN"` - State string `json:"state"` - Nameservers []string `json:"nameservers"` -} - -type ZoneProperties struct { - ZoneName string `json:"zoneName"` - Description string `json:"description"` - Enabled bool `json:"enabled"` -} - -type RecordResponse struct { - ID string `json:"id"` - Type string `json:"type"` - Metadata RecordMetadata `json:"metadata"` - Properties RecordProperties `json:"properties"` -} - -type RecordMetadata struct { - CreatedDate time.Time `json:"createdDate"` - CreatedBy string `json:"createdBy"` - CreatedByUserID string `json:"createdByUserId"` - LastModifiedDate time.Time `json:"lastModifiedDate"` - LastModifiedBy string `json:"lastModifiedBy"` - LastModifiedByUserID string `json:"lastModifiedByUserId"` - ResourceURN string `json:"resourceURN"` - State string `json:"state"` - Fqdn string `json:"fqdn"` - ZoneID string `json:"zoneId"` -} - -type RecordProperties struct { - Name string `json:"name"` - Type string `json:"type,omitempty"` - Content string `json:"content,omitempty"` - TTL int `json:"ttl,omitempty"` - Priority int `json:"priority,omitempty"` - Enabled bool `json:"enabled,omitempty"` -} diff --git a/providers/dns/ionoscloud/ionoscloud.go b/providers/dns/ionoscloud/ionoscloud.go deleted file mode 100644 index 0c33fba9f..000000000 --- a/providers/dns/ionoscloud/ionoscloud.go +++ /dev/null @@ -1,184 +0,0 @@ -// Package ionoscloud implements a DNS provider for solving the DNS-01 challenge using Ionos Cloud. -package ionoscloud - -import ( - "context" - "errors" - "fmt" - "net/http" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/ionoscloud/internal" -) - -// Environment variables names. -const ( - envNamespace = "IONOSCLOUD_" - - EnvAPIToken = envNamespace + "API_TOKEN" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIToken string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client - - zoneIDs map[string]string - recordIDs map[string]string - recordIDsMu sync.Mutex -} - -// NewDNSProvider returns a DNSProvider instance configured for Ionos Cloud. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIToken) - if err != nil { - return nil, fmt.Errorf("ionoscloud: %w", err) - } - - config := NewDefaultConfig() - config.APIToken = values[EnvAPIToken] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Ionos Cloud. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("ionoscloud: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.APIToken) - if err != nil { - return nil, fmt.Errorf("ionoscloud: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - zoneIDs: make(map[string]string), - recordIDs: make(map[string]string), - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("ionoscloud: could not find zone for domain %q: %w", domain, err) - } - - zones, err := d.client.RetrieveZones(ctx, dns01.UnFqdn(authZone)) - if err != nil { - return fmt.Errorf("ionoscloud: retrieve zones: %w", err) - } - - if len(zones) != 1 { - return fmt.Errorf("ionoscloud: zone ID not found for domain %q", domain) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("ionoscloud: %w", err) - } - - zoneID := zones[0].ID - - request := internal.RecordProperties{ - Name: subDomain, - Type: "TXT", - Content: info.Value, - TTL: d.config.TTL, - } - - record, err := d.client.CreateRecord(ctx, zoneID, request) - if err != nil { - return fmt.Errorf("ionoscloud: create record: %w", err) - } - - d.recordIDsMu.Lock() - d.zoneIDs[token] = zoneID - d.recordIDs[token] = record.ID - d.recordIDsMu.Unlock() - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - d.recordIDsMu.Lock() - zoneID, ok := d.zoneIDs[token] - d.recordIDsMu.Unlock() - - if !ok { - return fmt.Errorf("ionoscloud: unknown zone ID for '%s' '%s'", info.EffectiveFQDN, token) - } - - d.recordIDsMu.Lock() - recordID, ok := d.recordIDs[token] - d.recordIDsMu.Unlock() - - if !ok { - return fmt.Errorf("ionoscloud: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) - } - - err := d.client.DeleteRecord(context.Background(), zoneID, recordID) - if err != nil { - return fmt.Errorf("ionoscloud: delete record: %w", err) - } - - d.recordIDsMu.Lock() - delete(d.zoneIDs, token) - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/ionoscloud/ionoscloud.toml b/providers/dns/ionoscloud/ionoscloud.toml deleted file mode 100644 index 6e1d080e4..000000000 --- a/providers/dns/ionoscloud/ionoscloud.toml +++ /dev/null @@ -1,22 +0,0 @@ -Name = "Ionos Cloud" -Description = '''''' -URL = "https://cloud.ionos.de/network/cloud-dns" -Code = "ionoscloud" -Since = "v4.30.0" - -Example = ''' -IONOSCLOUD_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns ionoscloud -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - IONOSCLOUD_API_TOKEN = "API token" - [Configuration.Additional] - IONOSCLOUD_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - IONOSCLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - IONOSCLOUD_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - IONOSCLOUD_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://api.ionos.com/docs/dns/v1/" diff --git a/providers/dns/ionoscloud/ionoscloud_test.go b/providers/dns/ionoscloud/ionoscloud_test.go deleted file mode 100644 index 8282e08fc..000000000 --- a/providers/dns/ionoscloud/ionoscloud_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package ionoscloud - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAPIToken).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAPIToken: "secret", - }, - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "ionoscloud: some credentials information are missing: IONOSCLOUD_API_TOKEN", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiToken string - expected string - }{ - { - desc: "success", - apiToken: "secret", - }, - { - desc: "missing credentials", - expected: "ionoscloud: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIToken = test.apiToken - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIToken = "secret" - config.HTTPClient = server.Client() - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - p.client.BaseURL, _ = url.Parse(server.URL) - - return p, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(). - WithAuthorization("Bearer secret"), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("GET /zones", - servermock.ResponseFromInternal("zones.json"), - servermock.CheckQueryParameter().Strict(). - With("filter.zoneName", "example.com")). - Route("POST /zones/e74d0d15-f567-4b7b-9069-26ee1f93bae3/records", - servermock.ResponseFromInternal("create_record.json"), - servermock.CheckRequestJSONBodyFromInternal("create_record-request.json")). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("DELETE /zones/e74d0d15-f567-4b7b-9069-26ee1f93bae3/records/90d81ac0-3a30-44d4-95a5-12959effa6ee", - servermock.Noop(). - WithStatusCode(http.StatusAccepted)). - Build(t) - - token := "abc" - - provider.zoneIDs[token] = "e74d0d15-f567-4b7b-9069-26ee1f93bae3" - provider.recordIDs[token] = "90d81ac0-3a30-44d4-95a5-12959effa6ee" - - err := provider.CleanUp("example.com", token, "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/ipv64/internal/client.go b/providers/dns/ipv64/internal/client.go index 0dfd94374..fbb871aa3 100644 --- a/providers/dns/ipv64/internal/client.go +++ b/providers/dns/ipv64/internal/client.go @@ -34,7 +34,7 @@ func NewClient(hc *http.Client) *Client { } } -func (c *Client) GetDomains(ctx context.Context) (*Domains, error) { +func (c Client) GetDomains(ctx context.Context) (*Domains, error) { endpoint := c.baseURL.JoinPath("api") query := endpoint.Query() @@ -56,7 +56,7 @@ func (c *Client) GetDomains(ctx context.Context) (*Domains, error) { return results, nil } -func (c *Client) AddRecord(ctx context.Context, domain, prefix, recordType, content string) error { +func (c Client) AddRecord(ctx context.Context, domain, prefix, recordType, content string) error { endpoint := c.baseURL.JoinPath("api") data := make(url.Values) @@ -73,7 +73,7 @@ func (c *Client) AddRecord(ctx context.Context, domain, prefix, recordType, cont return c.do(req, nil) } -func (c *Client) DeleteRecord(ctx context.Context, domain, prefix, recordType, content string) error { +func (c Client) DeleteRecord(ctx context.Context, domain, prefix, recordType, content string) error { endpoint := c.baseURL.JoinPath("api") data := make(url.Values) @@ -90,7 +90,7 @@ func (c *Client) DeleteRecord(ctx context.Context, domain, prefix, recordType, c return c.do(req, nil) } -func (c *Client) do(req *http.Request, result any) error { +func (c Client) do(req *http.Request, result any) error { if req.Method != http.MethodGet { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") } @@ -131,7 +131,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) errAPI := &APIError{} - err := json.Unmarshal(raw, errAPI) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/ipv64/internal/client_test.go b/providers/dns/ipv64/internal/client_test.go index ba5ede9fc..1966f9f68 100644 --- a/providers/dns/ipv64/internal/client_test.go +++ b/providers/dns/ipv64/internal/client_test.go @@ -1,35 +1,69 @@ package internal import ( + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const testAPIKey = "secret" -func setupClient(server *httptest.Server) (*Client, error) { +func setupTest(t *testing.T, handler http.HandlerFunc) *Client { + t.Helper() + + server := httptest.NewServer(handler) + client := NewClient(OAuthStaticAccessToken(server.Client(), testAPIKey)) client.HTTPClient = server.Client() client.baseURL, _ = url.Parse(server.URL) - return client, nil + return client +} + +func testHandler(method, filename string, statusCode int) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + auth := req.Header.Get("Authorization") + if auth != "Bearer "+testAPIKey { + http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized) + return + } + + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = file.Close() }() + + rw.WriteHeader(statusCode) + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + } } func TestClient_GetDomains(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /api", - servermock.ResponseFromFixture("get_domains.json"), - servermock.CheckQueryParameter().Strict(). - With("get_domains", "")). - Build(t) + client := setupTest(t, testHandler(http.MethodGet, "get_domains.json", http.StatusOK)) - domains, err := client.GetDomains(t.Context()) + domains, err := client.GetDomains(context.Background()) require.NoError(t, err) expected := &Domains{ @@ -78,67 +112,38 @@ func TestClient_GetDomains(t *testing.T) { } func TestClient_GetDomains_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /api", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client := setupTest(t, testHandler(http.MethodGet, "error.json", http.StatusUnauthorized)) - domains, err := client.GetDomains(t.Context()) + domains, err := client.GetDomains(context.Background()) require.Error(t, err) require.Nil(t, domains) } func TestClient_AddRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient, - servermock.CheckHeader().WithContentTypeFromURLEncoded()). - Route("POST /api", - servermock.ResponseFromFixture("add_record.json"). - WithStatusCode(http.StatusCreated), - servermock.CheckForm().Strict(). - With("add_record", "lego.ipv64.net"). - With("content", "value"). - With("praefix", "_acme-challenge"). - With("type", "TXT"), - ). - Build(t) + client := setupTest(t, testHandler(http.MethodPost, "add_record.json", http.StatusCreated)) - err := client.AddRecord(t.Context(), "lego.ipv64.net", "_acme-challenge", "TXT", "value") + err := client.AddRecord(context.Background(), "lego.ipv64.net", "_acme-challenge", "TXT", "value") require.NoError(t, err) } func TestClient_AddRecord_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("POST /api", - servermock.ResponseFromFixture("add_record-error.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) + client := setupTest(t, testHandler(http.MethodPost, "add_record-error.json", http.StatusBadRequest)) - err := client.AddRecord(t.Context(), "lego.ipv64.net", "_acme-challenge", "TXT", "value") + err := client.AddRecord(context.Background(), "lego.ipv64.net", "_acme-challenge", "TXT", "value") require.Error(t, err) } func TestClient_DeleteRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient, - servermock.CheckHeader().WithContentTypeFromURLEncoded()). - Route("DELETE /api", - // the query parameters can be checked because the Go server ignores the body of a DELETE request. - servermock.ResponseFromFixture("del_record.json"). - WithStatusCode(http.StatusAccepted)). - Build(t) + client := setupTest(t, testHandler(http.MethodDelete, "del_record.json", http.StatusAccepted)) - err := client.DeleteRecord(t.Context(), "lego.ipv64.net", "_acme-challenge", "TXT", "value") + err := client.DeleteRecord(context.Background(), "lego.ipv64.net", "_acme-challenge", "TXT", "value") require.NoError(t, err) } func TestClient_DeleteRecord_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("DELETE /api", - servermock.ResponseFromFixture("del_record-error.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) + client := setupTest(t, testHandler(http.MethodDelete, "del_record-error.json", http.StatusBadRequest)) - err := client.DeleteRecord(t.Context(), "lego.ipv64.net", "_acme-challenge", "TXT", "value") + err := client.DeleteRecord(context.Background(), "lego.ipv64.net", "_acme-challenge", "TXT", "value") require.Error(t, err) } diff --git a/providers/dns/ipv64/internal/types.go b/providers/dns/ipv64/internal/types.go index 6ef31a3cc..e9e357ecc 100644 --- a/providers/dns/ipv64/internal/types.go +++ b/providers/dns/ipv64/internal/types.go @@ -11,7 +11,6 @@ type APIResponse struct { type APIError struct { APIResponse - AddRecordMessage string `json:"add_record"` DelRecordMessage string `json:"del_record"` AddDomainMessage string `json:"add_domain"` @@ -42,7 +41,6 @@ func (a APIError) Error() string { type Domains struct { APIResponse - APICall string `json:"add_domain"` Subdomains map[string]Subdomain `json:"subdomains"` } diff --git a/providers/dns/ipv64/ipv64.go b/providers/dns/ipv64/ipv64.go index 078fe5ca1..578614bce 100644 --- a/providers/dns/ipv64/ipv64.go +++ b/providers/dns/ipv64/ipv64.go @@ -9,10 +9,8 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/ipv64/internal" "github.com/miekg/dns" ) @@ -26,10 +24,9 @@ const ( EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" + EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" // Deprecated: unused, will be removed in v5. ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string @@ -86,8 +83,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{config: config, client: client}, nil } diff --git a/providers/dns/ipv64/ipv64.toml b/providers/dns/ipv64/ipv64.toml index aa1720c9e..6bcf841f0 100644 --- a/providers/dns/ipv64/ipv64.toml +++ b/providers/dns/ipv64/ipv64.toml @@ -6,16 +6,17 @@ Since = "v4.13.0" Example = ''' IPV64_API_KEY=xxxxxx \ -lego --dns ipv64 -d '*.example.com' -d example.com run +lego --email you@example.com --dns ipv64 --domains my.example.org run ''' [Configuration] [Configuration.Credentials] IPV64_API_KEY = "Account API Key" [Configuration.Additional] - IPV64_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - IPV64_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - IPV64_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + IPV64_POLLING_INTERVAL = "Time between DNS propagation check" + IPV64_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + IPV64_TTL = "The TTL of the TXT record used for the DNS challenge" + IPV64_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://ipv64.net/dyndns_updater_api" diff --git a/providers/dns/ipv64/ipv64_test.go b/providers/dns/ipv64/ipv64_test.go index 6dc7d1cfc..b3fe142e9 100644 --- a/providers/dns/ipv64/ipv64_test.go +++ b/providers/dns/ipv64/ipv64_test.go @@ -114,7 +114,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -172,7 +171,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -186,7 +184,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/ispconfig/internal/client.go b/providers/dns/ispconfig/internal/client.go deleted file mode 100644 index 9280fdec1..000000000 --- a/providers/dns/ispconfig/internal/client.go +++ /dev/null @@ -1,318 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" -) - -type Client struct { - serverURL string - HTTPClient *http.Client -} - -func NewClient(serverURL string) (*Client, error) { - _, err := url.Parse(serverURL) - if err != nil { - return nil, fmt.Errorf("server URL: %w", err) - } - - return &Client{ - serverURL: serverURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -func (c *Client) Login(ctx context.Context, username, password string) (string, error) { - payload := LoginRequest{ - Username: username, - Password: password, - ClientLogin: false, - } - - endpoint, err := url.Parse(c.serverURL) - if err != nil { - return "", err - } - - endpoint.RawQuery = "login" - - req, err := newJSONRequest(ctx, endpoint, payload) - if err != nil { - return "", err - } - - var response APIResponse - - err = c.do(req, &response) - if err != nil { - return "", err - } - - return extractResponse[string](response) -} - -func (c *Client) GetClientID(ctx context.Context, sessionID, sysUserID string) (int, error) { - payload := ClientIDRequest{ - SessionID: sessionID, - SysUserID: sysUserID, - } - - endpoint, err := url.Parse(c.serverURL) - if err != nil { - return 0, err - } - - endpoint.RawQuery = "client_get_id" - - req, err := newJSONRequest(ctx, endpoint, payload) - if err != nil { - return 0, err - } - - var response APIResponse - - err = c.do(req, &response) - if err != nil { - return 0, err - } - - return extractResponse[int](response) -} - -// GetZoneID returns the zone ID for the given name. -func (c *Client) GetZoneID(ctx context.Context, sessionID, name string) (int, error) { - payload := map[string]any{ - "session_id": sessionID, - "origin": name, - } - - endpoint, err := url.Parse(c.serverURL) - if err != nil { - return 0, err - } - - endpoint.RawQuery = "dns_zone_get_id" - - req, err := newJSONRequest(ctx, endpoint, payload) - if err != nil { - return 0, err - } - - var response APIResponse - - err = c.do(req, &response) - if err != nil { - return 0, err - } - - return extractResponse[int](response) -} - -// GetZone returns the zone information for the zone ID. -func (c *Client) GetZone(ctx context.Context, sessionID, zoneID string) (*Zone, error) { - payload := map[string]any{ - "session_id": sessionID, - "primary_id": zoneID, - } - - endpoint, err := url.Parse(c.serverURL) - if err != nil { - return nil, err - } - - endpoint.RawQuery = "dns_zone_get" - - req, err := newJSONRequest(ctx, endpoint, payload) - if err != nil { - return nil, err - } - - var response APIResponse - - err = c.do(req, &response) - if err != nil { - return nil, err - } - - return extractResponse[*Zone](response) -} - -// GetTXT returns the TXT record for the given name. -// `name` must be a fully qualified domain name, e.g. "example.com.". -func (c *Client) GetTXT(ctx context.Context, sessionID, name string) (*Record, error) { - payload := GetTXTRequest{ - SessionID: sessionID, - PrimaryID: struct { - Name string `json:"name"` - Type string `json:"type"` - }{ - Name: name, - Type: "txt", - }, - } - - endpoint, err := url.Parse(c.serverURL) - if err != nil { - return nil, err - } - - endpoint.RawQuery = "dns_txt_get" - - req, err := newJSONRequest(ctx, endpoint, payload) - if err != nil { - return nil, err - } - - var response APIResponse - - err = c.do(req, &response) - if err != nil { - return nil, err - } - - return extractResponse[*Record](response) -} - -// AddTXT adds a TXT record. -// It returns the ID of the newly created record. -func (c *Client) AddTXT(ctx context.Context, sessionID, clientID string, params RecordParams) (string, error) { - payload := AddTXTRequest{ - SessionID: sessionID, - ClientID: clientID, - Params: ¶ms, - UpdateSerial: true, - } - - endpoint, err := url.Parse(c.serverURL) - if err != nil { - return "", err - } - - endpoint.RawQuery = "dns_txt_add" - - req, err := newJSONRequest(ctx, endpoint, payload) - if err != nil { - return "", err - } - - var response APIResponse - - err = c.do(req, &response) - if err != nil { - return "", err - } - - return extractResponse[string](response) -} - -// DeleteTXT deletes a TXT record. -// It returns the number of deleted records. -func (c *Client) DeleteTXT(ctx context.Context, sessionID, recordID string) (int, error) { - payload := DeleteTXTRequest{ - SessionID: sessionID, - PrimaryID: recordID, - UpdateSerial: true, - } - - endpoint, err := url.Parse(c.serverURL) - if err != nil { - return 0, err - } - - endpoint.RawQuery = "dns_txt_delete" - - req, err := newJSONRequest(ctx, endpoint, payload) - if err != nil { - return 0, err - } - - var response APIResponse - - err = c.do(req, &response) - if err != nil { - return 0, err - } - - return extractResponse[int](response) -} - -func (c *Client) do(req *http.Request, result any) error { - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - raw, _ := io.ReadAll(resp.Body) - - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} - -func extractResponse[T any](response APIResponse) (T, error) { - if response.Code != "ok" { - var zero T - - return zero, &APIError{APIResponse: response} - } - - var result T - - err := json.Unmarshal(response.Response, &result) - if err != nil { - var zero T - return zero, fmt.Errorf("unable to unmarshal response: %s, %w", string(response.Response), err) - } - - return result, nil -} diff --git a/providers/dns/ispconfig/internal/client_test.go b/providers/dns/ispconfig/internal/client_test.go deleted file mode 100644 index a4db3d5f7..000000000 --- a/providers/dns/ispconfig/internal/client_test.go +++ /dev/null @@ -1,175 +0,0 @@ -package internal - -import ( - "net/http/httptest" - "testing" - "time" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient(server.URL) - if err != nil { - return nil, err - } - - client.HTTPClient = server.Client() - - return client, nil - }) -} - -func TestClient_Login(t *testing.T) { - client := mockBuilder(). - Route("POST /", - servermock.ResponseFromFixture("login.json"), - servermock.CheckRequestJSONBodyFromFixture("login-request.json"), - servermock.CheckQueryParameter().Strict(). - With("login", ""), - ). - Build(t) - - sessionID, err := client.Login(t.Context(), "user", "secret") - require.NoError(t, err) - - assert.Equal(t, "abc", sessionID) -} - -func TestClient_Login_error(t *testing.T) { - client := mockBuilder(). - Route("POST /", - servermock.ResponseFromFixture("error.json"), - ). - Build(t) - - _, err := client.Login(t.Context(), "user", "secret") - require.EqualError(t, err, `code: remote_fault, message: The login failed. Username or password wrong., response: false`) -} - -func TestClient_GetClientID(t *testing.T) { - client := mockBuilder(). - Route("POST /", - servermock.ResponseFromFixture("client_get_id.json"), - servermock.CheckRequestJSONBodyFromFixture("client_get_id-request.json"), - servermock.CheckQueryParameter().Strict(). - With("client_get_id", ""), - ). - Build(t) - - id, err := client.GetClientID(t.Context(), "sessionA", "sysA") - require.NoError(t, err) - - assert.Equal(t, 123, id) -} - -func TestClient_GetZoneID(t *testing.T) { - client := mockBuilder(). - Route("POST /", - servermock.ResponseFromFixture("dns_zone_get_id.json"), - servermock.CheckRequestJSONBodyFromFixture("dns_zone_get_id-request.json"), - servermock.CheckQueryParameter().Strict(). - With("dns_zone_get_id", ""), - ). - Build(t) - - zoneID, err := client.GetZoneID(t.Context(), "sessionA", "example.com") - require.NoError(t, err) - - assert.Equal(t, 123, zoneID) -} - -func TestClient_GetZone(t *testing.T) { - client := mockBuilder(). - Route("POST /", - servermock.ResponseFromFixture("dns_zone_get.json"), - servermock.CheckRequestJSONBodyFromFixture("dns_zone_get-request.json"), - servermock.CheckQueryParameter().Strict(). - With("dns_zone_get", ""), - ). - Build(t) - - zone, err := client.GetZone(t.Context(), "sessionA", "example.com.") - require.NoError(t, err) - - expected := &Zone{ - ID: "456", - ServerID: "123", - SysUserID: "789", - SysGroupID: "2", - Origin: "example.com.", - Serial: "2025102902", - Active: "Y", - } - - assert.Equal(t, expected, zone) -} - -func TestClient_GetTXT(t *testing.T) { - client := mockBuilder(). - Route("POST /", - servermock.ResponseFromFixture("dns_txt_get.json"), - servermock.CheckRequestJSONBodyFromFixture("dns_txt_get-request.json"), - servermock.CheckQueryParameter().Strict(). - With("dns_txt_get", ""), - ). - Build(t) - - record, err := client.GetTXT(t.Context(), "sessionA", "example.com.") - require.NoError(t, err) - - expected := &Record{ID: 123} - - assert.Equal(t, expected, record) -} - -func TestClient_AddTXT(t *testing.T) { - client := mockBuilder(). - Route("POST /", - servermock.ResponseFromFixture("dns_txt_add.json"), - servermock.CheckRequestJSONBodyFromFixture("dns_txt_add-request.json"), - servermock.CheckQueryParameter().Strict(). - With("dns_txt_add", ""), - ). - Build(t) - - now := time.Date(2025, 12, 25, 1, 1, 1, 0, time.UTC) - - params := RecordParams{ - ServerID: "serverA", - Zone: "example.com.", - Name: "foo.example.com.", - Type: "txt", - Data: "txtTXTtxt", - Aux: "0", - TTL: "3600", - Active: "y", - Stamp: now.Format("2006-01-02 15:04:05"), - UpdateSerial: true, - } - - recordID, err := client.AddTXT(t.Context(), "sessionA", "clientA", params) - require.NoError(t, err) - - assert.Equal(t, "123", recordID) -} - -func TestClient_DeleteTXT(t *testing.T) { - client := mockBuilder(). - Route("POST /", - servermock.ResponseFromFixture("dns_txt_delete.json"), - servermock.CheckRequestJSONBodyFromFixture("dns_txt_delete-request.json"), - servermock.CheckQueryParameter().Strict(). - With("dns_txt_delete", ""), - ). - Build(t) - - count, err := client.DeleteTXT(t.Context(), "sessionA", "123") - require.NoError(t, err) - - assert.Equal(t, 1, count) -} diff --git a/providers/dns/ispconfig/internal/fixtures/client_get_id-request.json b/providers/dns/ispconfig/internal/fixtures/client_get_id-request.json deleted file mode 100644 index ba573f824..000000000 --- a/providers/dns/ispconfig/internal/fixtures/client_get_id-request.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "session_id": "sessionA", - "sys_userid": "sysA" -} diff --git a/providers/dns/ispconfig/internal/fixtures/client_get_id.json b/providers/dns/ispconfig/internal/fixtures/client_get_id.json deleted file mode 100644 index 7b9f667a0..000000000 --- a/providers/dns/ispconfig/internal/fixtures/client_get_id.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "ok", - "message": "foo", - "response": 123 -} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_txt_add-request.json b/providers/dns/ispconfig/internal/fixtures/dns_txt_add-request.json deleted file mode 100644 index bf5242cd1..000000000 --- a/providers/dns/ispconfig/internal/fixtures/dns_txt_add-request.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "session_id": "sessionA", - "client_id": "clientA", - "params": { - "server_id": "serverA", - "zone": "example.com.", - "name": "foo.example.com.", - "type": "txt", - "data": "txtTXTtxt", - "aux": "0", - "ttl": "3600", - "active": "y", - "stamp": "2025-12-25 01:01:01", - "update_serial": true - }, - "update_serial": true -} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_txt_add.json b/providers/dns/ispconfig/internal/fixtures/dns_txt_add.json deleted file mode 100644 index 7980619fe..000000000 --- a/providers/dns/ispconfig/internal/fixtures/dns_txt_add.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "ok", - "message": "foo", - "response": "123" -} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_txt_delete-request.json b/providers/dns/ispconfig/internal/fixtures/dns_txt_delete-request.json deleted file mode 100644 index 240976654..000000000 --- a/providers/dns/ispconfig/internal/fixtures/dns_txt_delete-request.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "session_id": "sessionA", - "primary_id": "123", - "update_serial": true -} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_txt_delete.json b/providers/dns/ispconfig/internal/fixtures/dns_txt_delete.json deleted file mode 100644 index 960b650bd..000000000 --- a/providers/dns/ispconfig/internal/fixtures/dns_txt_delete.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "ok", - "message": "foo", - "response": 1 -} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_txt_get-request.json b/providers/dns/ispconfig/internal/fixtures/dns_txt_get-request.json deleted file mode 100644 index 8bda44067..000000000 --- a/providers/dns/ispconfig/internal/fixtures/dns_txt_get-request.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "session_id": "sessionA", - "primary_id": { - "name": "example.com.", - "type": "txt" - } -} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_txt_get.json b/providers/dns/ispconfig/internal/fixtures/dns_txt_get.json deleted file mode 100644 index f707d50c3..000000000 --- a/providers/dns/ispconfig/internal/fixtures/dns_txt_get.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "code": "ok", - "message": "foo", - "response": { - "id": 123 - } -} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_zone_get-request.json b/providers/dns/ispconfig/internal/fixtures/dns_zone_get-request.json deleted file mode 100644 index 3d44d468f..000000000 --- a/providers/dns/ispconfig/internal/fixtures/dns_zone_get-request.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "primary_id": "example.com.", - "session_id": "sessionA" -} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_zone_get.json b/providers/dns/ispconfig/internal/fixtures/dns_zone_get.json deleted file mode 100644 index 37975d0e6..000000000 --- a/providers/dns/ispconfig/internal/fixtures/dns_zone_get.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "code": "ok", - "message": "foo", - "response": { - "id": "456", - "sys_userid": "789", - "sys_groupid": "2", - "sys_perm_user": "riud", - "sys_perm_group": "riud", - "sys_perm_other": "", - "server_id": "123", - "origin": "example.com.", - "ns": "ns1.example.org.", - "mbox": "support.example.net.", - "serial": "2025102902", - "refresh": "7200", - "retry": "540", - "expire": "604800", - "minimum": "3600", - "ttl": "3600", - "active": "Y", - "xfer": "", - "also_notify": "", - "update_acl": "", - "dnssec_initialized": "N", - "dnssec_wanted": "N", - "dnssec_algo": "ECDSAP256SHA256", - "dnssec_last_signed": "0", - "dnssec_info": "", - "rendered_zone": "" - } -} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_zone_get_id-request.json b/providers/dns/ispconfig/internal/fixtures/dns_zone_get_id-request.json deleted file mode 100644 index e3084242e..000000000 --- a/providers/dns/ispconfig/internal/fixtures/dns_zone_get_id-request.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "origin": "example.com", - "session_id": "sessionA" -} diff --git a/providers/dns/ispconfig/internal/fixtures/dns_zone_get_id.json b/providers/dns/ispconfig/internal/fixtures/dns_zone_get_id.json deleted file mode 100644 index 7b9f667a0..000000000 --- a/providers/dns/ispconfig/internal/fixtures/dns_zone_get_id.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "ok", - "message": "foo", - "response": 123 -} diff --git a/providers/dns/ispconfig/internal/fixtures/error.json b/providers/dns/ispconfig/internal/fixtures/error.json deleted file mode 100644 index a9c76546c..000000000 --- a/providers/dns/ispconfig/internal/fixtures/error.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "remote_fault", - "message": "The login failed. Username or password wrong.", - "response": false -} diff --git a/providers/dns/ispconfig/internal/fixtures/login-request.json b/providers/dns/ispconfig/internal/fixtures/login-request.json deleted file mode 100644 index c3293a2e8..000000000 --- a/providers/dns/ispconfig/internal/fixtures/login-request.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "username": "user", - "password": "secret", - "client_login": false -} diff --git a/providers/dns/ispconfig/internal/fixtures/login.json b/providers/dns/ispconfig/internal/fixtures/login.json deleted file mode 100644 index e380a86ec..000000000 --- a/providers/dns/ispconfig/internal/fixtures/login.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "ok", - "message": "foo", - "response": "abc" -} diff --git a/providers/dns/ispconfig/internal/readme.md b/providers/dns/ispconfig/internal/readme.md deleted file mode 100644 index 2284c338f..000000000 --- a/providers/dns/ispconfig/internal/readme.md +++ /dev/null @@ -1,249 +0,0 @@ -## Error Response - -```json -{ - "code": "", - "message": "", - "response": false -} -``` - -## Login Endpoint - -* URL: `?login` -* HTTP Method: `POST` - -- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/login.html -- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/login.php - -### Request Body (JSON) - -```json -{ - "username": "", - "password": "", - "client_login": false -} -``` - -### Response Body (JSON) - -```json -{ - "code": "ok", - "message": "foo", - "response": "abc" -} -``` - -- `response`: is the `sessionID` - -## Get Client ID Endpoint - -* URL: `?client_get_id` -* HTTP Method: `POST` - -- function `client_get_id`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/client.inc.php#L97 -- TABLE `sys_user`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/install/sql/ispconfig3.sql?ref_type=heads#L1852 -- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/client_get_id.html -- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/client_get_id.php - -### Request Body (JSON) - -```json -{ - "session_id": "", - "sys_userid": "" -} -``` - -### Response Body (JSON) - -```json -{ - "code": "ok", - "message": "foo", - "response": 123 -} -``` - -## DNS Zone Get ID Endpoint - -* URL: `?dns_zone_get_id` -* HTTP Method: `POST` - -- function `dns_zone_get_id`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L142 -- TABLE `dns_soa`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/install/sql/ispconfig3.sql?ref_type=heads#L615 - -### Request Body (JSON) - -```json -{ - "session_id": "", - "origin": "" -} -``` - -### Response Body (JSON) - -```json -{ - "code": "ok", - "message": "foo", - "response": 123 -} -``` - -## DNS Zone Get Endpoint - -* URL: `?dns_zone_get` -* HTTP Method: `POST` - -- function `dns_zone_get`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L87 -- function `getDataRecord`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remoting_lib.inc.php#L248 -- TABLE `dns_soa`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/install/sql/ispconfig3.sql?ref_type=heads#L615 -- Depending on the request, the response may be an array or an object (`primary_id` can be a string, an array or an object). -- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/dns_zone_get.html -- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/dns_zone_get.php - -### Request Body (JSON) - -```json -{ - "session_id": "", - "primary_id": "" -} -``` - -### Response Body (JSON) - -```json -{ - "code": "ok", - "message": "foo", - "response": { - "id": 456, - "server_id": 123, - "sys_userid": 789 - } -} -``` - -## DNS TXT Get Endpoint - -* URL: `?dns_txt_get` -* HTTP Method: `POST` - -- function `dns_txt_get`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L640 -- function `dns_rr_get`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L195 -- form: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/web/dns/form/dns_txt.tform.php -- TABLE `dns_rr`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/install/sql/ispconfig3.sql?ref_type=heads#L490 -- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/dns_txt_get.html -- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/dns_txt_get.php - -### Request Body (JSON) - -```json -{ - "session_id": "", - "primary_id": { - "name": ".", - "type": "TXT" - } -} -``` - -### Response Body (JSON) - -```json -{ - "code": "ok", - "message": "foo", - "response": { - "id": 123 - } -} -``` - -## DNS TXT Add Endpoint - -* URL: `?dns_txt_add` -* HTTP Method: `POST` - -- function `dns_txt_add`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L645 -- function `dns_rr_add` https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L212 -- form: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/web/dns/form/dns_txt.tform.php -- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/dns_txt_add.html -- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/dns_txt_add.php - -### Request Body (JSON) - -```json -{ - "session_id": "", - "client_id": "", - "params": { - "server_id": "", - "zone": "", - "name": ".", - "type": "txt", - "data": "", - "aux": "0", - "ttl": "3600", - "active": "y", - "stamp": "", - "update_serial": true - }, - "update_serial": true -} -``` - -- `stamp`: (ex: `2025-12-17 23:35:58`) -- `serial`: (ex: `1766010947`) - -### Response Body (JSON) - -```json -{ - "code": "ok", - "message": "foo", - "response": "123" -} -``` - -## DNS TXT Delete Endpoint - -* URL: `?dns_txt_delete` -* HTTP Method: `POST` - -- function `dns_txt_delete`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L655 -- function `dns_rr_delete`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L247 -- form: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/web/dns/form/dns_txt.tform.php -- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/dns_txt_delete.html -- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/dns_txt_delete.php - -### Request Body (JSON) - -```json -{ - "session_id": "", - "primary_id": "", - "update_serial": true -} -``` - -### Response Body (JSON) - -```json -{ - "code": "ok", - "message": "foo", - "response": 1 -} -``` - ---- - -https://www.ispconfig.org/ -https://git.ispconfig.org/ispconfig/ispconfig3 -https://forum.howtoforge.com/#ispconfig-3.23 diff --git a/providers/dns/ispconfig/internal/types.go b/providers/dns/ispconfig/internal/types.go deleted file mode 100644 index 7db0846cc..000000000 --- a/providers/dns/ispconfig/internal/types.go +++ /dev/null @@ -1,95 +0,0 @@ -package internal - -import ( - "encoding/json" - "strings" -) - -type APIError struct { - APIResponse -} - -func (e *APIError) Error() string { - var msg strings.Builder - - msg.WriteString("code: " + e.Code) - - if e.Message != "" { - msg.WriteString(", message: " + e.Message) - } - - if len(e.Response) > 0 { - msg.WriteString(", response: " + string(e.Response)) - } - - return msg.String() -} - -type APIResponse struct { - Code string `json:"code"` - Message string `json:"message"` - Response json.RawMessage `json:"response"` -} - -type LoginRequest struct { - Username string `json:"username"` - Password string `json:"password"` - ClientLogin bool `json:"client_login"` -} - -type ClientIDRequest struct { - SessionID string `json:"session_id"` - SysUserID string `json:"sys_userid"` -} - -type Zone struct { - ID string `json:"id"` - ServerID string `json:"server_id"` - SysUserID string `json:"sys_userid"` - SysGroupID string `json:"sys_groupid"` - Origin string `json:"origin"` - Serial string `json:"serial"` - Active string `json:"active"` -} - -type GetTXTRequest struct { - SessionID string `json:"session_id"` - PrimaryID struct { - Name string `json:"name"` - Type string `json:"type"` - } `json:"primary_id"` -} - -type Record struct { - ID int `json:"id"` -} - -type AddTXTRequest struct { - SessionID string `json:"session_id"` - ClientID string `json:"client_id"` - Params *RecordParams `json:"params,omitempty"` - UpdateSerial bool `json:"update_serial"` -} - -type RecordParams struct { - ServerID string `json:"server_id"` - Zone string `json:"zone"` - Name string `json:"name"` - // 'a','aaaa','alias','cname','hinfo','mx','naptr','ns','ds','ptr','rp','srv','txt' - Type string `json:"type"` - Data string `json:"data"` - // "0" - Aux string `json:"aux"` - TTL string `json:"ttl"` - // 'n','y' - Active string `json:"active"` - // `2025-12-17 23:35:58` - Stamp string `json:"stamp"` - UpdateSerial bool `json:"update_serial"` -} - -type DeleteTXTRequest struct { - SessionID string `json:"session_id"` - PrimaryID string `json:"primary_id"` - UpdateSerial bool `json:"update_serial"` -} diff --git a/providers/dns/ispconfig/ispconfig.go b/providers/dns/ispconfig/ispconfig.go deleted file mode 100644 index 9396430b7..000000000 --- a/providers/dns/ispconfig/ispconfig.go +++ /dev/null @@ -1,220 +0,0 @@ -// Package ispconfig implements a DNS provider for solving the DNS-01 challenge using ISPConfig. -package ispconfig - -import ( - "context" - "crypto/tls" - "errors" - "fmt" - "net/http" - "strconv" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/ispconfig/internal" -) - -// Environment variables names. -const ( - envNamespace = "ISPCONFIG_" - - EnvServerURL = envNamespace + "SERVER_URL" - EnvUsername = envNamespace + "USERNAME" - EnvPassword = envNamespace + "PASSWORD" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" - EnvInsecureSkipVerify = envNamespace + "INSECURE_SKIP_VERIFY" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - ServerURL string - Username string - Password string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client - InsecureSkipVerify bool -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client - - recordIDs map[string]string - recordIDsMu sync.Mutex -} - -// NewDNSProvider returns a DNSProvider instance configured for ISPConfig. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvServerURL, EnvUsername, EnvPassword) - if err != nil { - return nil, fmt.Errorf("ispconfig: %w", err) - } - - config := NewDefaultConfig() - config.ServerURL = values[EnvServerURL] - config.Username = values[EnvUsername] - config.Password = values[EnvPassword] - config.InsecureSkipVerify = env.GetOrDefaultBool(EnvInsecureSkipVerify, false) - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for ISPConfig. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("ispconfig: the configuration of the DNS provider is nil") - } - - if config.ServerURL == "" { - return nil, errors.New("ispconfig: missing server URL") - } - - if config.Username == "" || config.Password == "" { - return nil, errors.New("ispconfig: credentials missing") - } - - client, err := internal.NewClient(config.ServerURL) - if err != nil { - return nil, fmt.Errorf("ispconfig: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - if config.InsecureSkipVerify { - client.HTTPClient.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - recordIDs: make(map[string]string), - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - sessionID, err := d.client.Login(ctx, d.config.Username, d.config.Password) - if err != nil { - return fmt.Errorf("ispconfig: login: %w", err) - } - - zoneID, err := d.findZone(ctx, sessionID, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("ispconfig: get zone id: %w", err) - } - - zone, err := d.client.GetZone(ctx, sessionID, strconv.Itoa(zoneID)) - if err != nil { - return fmt.Errorf("ispconfig: get zone: %w", err) - } - - clientID, err := d.client.GetClientID(ctx, sessionID, zone.SysUserID) - if err != nil { - return fmt.Errorf("ispconfig: get client id: %w", err) - } - - params := internal.RecordParams{ - ServerID: "serverA", - Zone: zone.ID, - Name: info.EffectiveFQDN, - Type: "txt", - Data: info.Value, - Aux: "0", - TTL: strconv.Itoa(d.config.TTL), - Active: "y", - Stamp: time.Now().UTC().Format("2006-01-02 15:04:05"), - } - - recordID, err := d.client.AddTXT(ctx, sessionID, strconv.Itoa(clientID), params) - if err != nil { - return fmt.Errorf("ispconfig: add txt record: %w", err) - } - - d.recordIDsMu.Lock() - d.recordIDs[token] = recordID - d.recordIDsMu.Unlock() - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - // gets the record's unique ID - d.recordIDsMu.Lock() - recordID, ok := d.recordIDs[token] - d.recordIDsMu.Unlock() - - if !ok { - return fmt.Errorf("ispconfig: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) - } - - sessionID, err := d.client.Login(ctx, d.config.Username, d.config.Password) - if err != nil { - return fmt.Errorf("ispconfig: login: %w", err) - } - - _, err = d.client.DeleteTXT(ctx, sessionID, recordID) - if err != nil { - return fmt.Errorf("ispconfig: delete txt record: %w", err) - } - - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -func (d *DNSProvider) findZone(ctx context.Context, sessionID, fqdn string) (int, error) { - for domain := range dns01.UnFqdnDomainsSeq(fqdn) { - zoneID, err := d.client.GetZoneID(ctx, sessionID, domain) - if err == nil { - return zoneID, nil - } - } - - return 0, fmt.Errorf("zone not found for %q", fqdn) -} diff --git a/providers/dns/ispconfig/ispconfig.toml b/providers/dns/ispconfig/ispconfig.toml deleted file mode 100644 index 4defd5509..000000000 --- a/providers/dns/ispconfig/ispconfig.toml +++ /dev/null @@ -1,27 +0,0 @@ -Name = "ISPConfig 3" -Description = '''''' -URL = "https://www.ispconfig.org/" -Code = "ispconfig" -Since = "v4.31.0" - -Example = ''' -ISPCONFIG_SERVER_URL="https://example.com:8080/remote/json.php" \ -ISPCONFIG_USERNAME="xxx" \ -ISPCONFIG_PASSWORD="yyy" \ -lego --dns ispconfig -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - ISPCONFIG_SERVER_URL = "Server URL" - ISPCONFIG_USERNAME = "Username" - ISPCONFIG_PASSWORD = "Password" - [Configuration.Additional] - ISPCONFIG_INSECURE_SKIP_VERIFY = "Whether to verify the API certificate" - ISPCONFIG_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - ISPCONFIG_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - ISPCONFIG_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - ISPCONFIG_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/index.html" diff --git a/providers/dns/ispconfig/ispconfig_test.go b/providers/dns/ispconfig/ispconfig_test.go deleted file mode 100644 index b03463aee..000000000 --- a/providers/dns/ispconfig/ispconfig_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package ispconfig - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest( - EnvServerURL, - EnvUsername, - EnvPassword, -).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvServerURL: "https://example.com:80/", - EnvUsername: "user", - EnvPassword: "secret", - }, - }, - { - desc: "missing server URL", - envVars: map[string]string{ - EnvServerURL: "", - EnvUsername: "user", - EnvPassword: "secret", - }, - expected: "ispconfig: some credentials information are missing: ISPCONFIG_SERVER_URL", - }, - { - desc: "missing username", - envVars: map[string]string{ - EnvServerURL: "https://example.com:80/", - EnvUsername: "", - EnvPassword: "secret", - }, - expected: "ispconfig: some credentials information are missing: ISPCONFIG_USERNAME", - }, - { - desc: "missing password", - envVars: map[string]string{ - EnvServerURL: "https://example.com:80/", - EnvUsername: "user", - EnvPassword: "", - }, - expected: "ispconfig: some credentials information are missing: ISPCONFIG_PASSWORD", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "ispconfig: some credentials information are missing: ISPCONFIG_SERVER_URL,ISPCONFIG_USERNAME,ISPCONFIG_PASSWORD", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - serverURL string - username string - password string - expected string - }{ - { - desc: "success", - serverURL: "https://example.com:80/", - username: "user", - password: "secret", - }, - { - desc: "missing server URL", - username: "user", - password: "secret", - expected: "ispconfig: missing server URL", - }, - { - desc: "missing username", - serverURL: "https://example.com:80/", - password: "secret", - expected: "ispconfig: credentials missing", - }, - { - desc: "missing password", - serverURL: "https://example.com:80/", - username: "user", - expected: "ispconfig: credentials missing", - }, - { - desc: "missing credentials", - expected: "ispconfig: missing server URL", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.ServerURL = test.serverURL - config.Username = test.username - config.Password = test.password - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/ispconfigddns/internal/client.go b/providers/dns/ispconfigddns/internal/client.go deleted file mode 100644 index 700b58f89..000000000 --- a/providers/dns/ispconfigddns/internal/client.go +++ /dev/null @@ -1,111 +0,0 @@ -package internal - -import ( - "context" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" - querystring "github.com/google/go-querystring/query" -) - -const ( - addAction = "add" - deleteAction = "delete" -) - -type Client struct { - token string - serverURL string - - HTTPClient *http.Client -} - -func NewClient(serverURL, token string) (*Client, error) { - _, err := url.Parse(serverURL) - if err != nil { - return nil, fmt.Errorf("server URL: %w", err) - } - - return &Client{ - serverURL: serverURL, - token: token, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -func (c *Client) AddTXTRecord(ctx context.Context, zone, fqdn, content string) error { - return c.updateRecord(ctx, UpdateRecord{Action: addAction, Zone: zone, Type: "TXT", Record: fqdn, Data: content}) -} - -func (c *Client) DeleteTXTRecord(ctx context.Context, zone, fqdn, recordContent string) error { - return c.updateRecord(ctx, UpdateRecord{Action: deleteAction, Zone: zone, Type: "TXT", Record: fqdn, Data: recordContent}) -} - -func (c *Client) updateRecord(ctx context.Context, action UpdateRecord) error { - req, err := c.newRequest(ctx, action) - if err != nil { - return err - } - - return c.do(req) -} - -func (c *Client) do(req *http.Request) error { - useragent.SetHeader(req.Header) - - req.SetBasicAuth("anonymous", c.token) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - // The endpoint uses the `DefaultDdnsResponseWriter`, - // and this writer uses HTTP status code to determine if the request was successful or not. - // - https://github.com/mhofer117/ispconfig-ddns-module/blob/8b011a5bb138881d9f13360a5c4fec10c0084613/lib/updater/DdnsUpdater.php#L53-L57 - // - https://github.com/mhofer117/ispconfig-ddns-module/blob/master/lib/updater/response/DefaultDdnsResponseWriter.php - if resp.StatusCode/100 != 2 { - raw, _ := io.ReadAll(resp.Body) - - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return nil -} - -func (c *Client) newRequest(ctx context.Context, action UpdateRecord) (*http.Request, error) { - endpoint, err := url.Parse(c.serverURL) - if err != nil { - return nil, err - } - - endpoint = endpoint.JoinPath("ddns", "update.php") - - values, err := querystring.Values(action) - if err != nil { - return nil, err - } - - endpoint.RawQuery = values.Encode() - - method := http.MethodPost - if action.Action == deleteAction { - method = http.MethodDelete - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), nil) - if err != nil { - return nil, err - } - - req.Header.Set("Accept", "application/json") - - return req, nil -} diff --git a/providers/dns/ispconfigddns/internal/client_test.go b/providers/dns/ispconfigddns/internal/client_test.go deleted file mode 100644 index 774e5ee46..000000000 --- a/providers/dns/ispconfigddns/internal/client_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -func setupClient(server *httptest.Server) (*Client, error) { - client, err := NewClient(server.URL, "secret") - if err != nil { - return nil, err - } - - client.HTTPClient = server.Client() - - return client, nil -} - -func TestClient_AddTXTRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("POST /ddns/update.php", - servermock.Noop(), - servermock.CheckHeader(). - WithBasicAuth("anonymous", "secret"), - servermock.CheckQueryParameter().Strict(). - With("action", "add"). - With("zone", "example.com"). - With("type", "TXT"). - With("record", "_acme-challenge.example.com."). - With("data", "token"), - ). - Build(t) - - err := client.AddTXTRecord(t.Context(), "example.com", "_acme-challenge.example.com.", "token") - require.NoError(t, err) -} - -func TestClient_AddTXTRecord_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("POST /ddns/update.php", - servermock.RawStringResponse("Missing or invalid token."). - WithStatusCode(http.StatusUnauthorized), - ). - Build(t) - - err := client.AddTXTRecord(t.Context(), "example.com", "_acme-challenge.example.com.", "token") - require.EqualError(t, err, "unexpected status code: [status code: 401] body: Missing or invalid token.") -} - -func TestClient_DeleteTXTRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("DELETE /ddns/update.php", - servermock.Noop(), - servermock.CheckHeader(). - WithBasicAuth("anonymous", "secret"), - servermock.CheckQueryParameter().Strict(). - With("action", "delete"). - With("zone", "example.com"). - With("type", "TXT"). - With("record", "_acme-challenge.example.com."). - With("data", "token"), - ). - Build(t) - - err := client.DeleteTXTRecord(t.Context(), "example.com", "_acme-challenge.example.com.", "token") - require.NoError(t, err) -} - -func TestClient_DeleteTXTRecord_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("DELETE /ddns/update.php", - servermock.RawStringResponse("Missing or invalid token."). - WithStatusCode(http.StatusUnauthorized), - ). - Build(t) - - err := client.DeleteTXTRecord(t.Context(), "example.com", "_acme-challenge.example.com.", "token") - require.EqualError(t, err, "unexpected status code: [status code: 401] body: Missing or invalid token.") -} diff --git a/providers/dns/ispconfigddns/internal/types.go b/providers/dns/ispconfigddns/internal/types.go deleted file mode 100644 index 278738108..000000000 --- a/providers/dns/ispconfigddns/internal/types.go +++ /dev/null @@ -1,9 +0,0 @@ -package internal - -type UpdateRecord struct { - Action string `url:"action,omitempty"` - Zone string `url:"zone,omitempty"` - Type string `url:"type,omitempty"` - Record string `url:"record,omitempty"` - Data string `url:"data,omitempty"` -} diff --git a/providers/dns/ispconfigddns/ispconfigddns.go b/providers/dns/ispconfigddns/ispconfigddns.go deleted file mode 100644 index eab5d413f..000000000 --- a/providers/dns/ispconfigddns/ispconfigddns.go +++ /dev/null @@ -1,145 +0,0 @@ -// Package ispconfigddns implements a DNS provider for solving the DNS-01 challenge using ISPConfig 3 Dynamic DNS (DDNS) Module. -package ispconfigddns - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/ispconfigddns/internal" -) - -// Environment variables names. -const ( - envNamespace = "ISPCONFIG_DDNS_" - - EnvServerURL = envNamespace + "SERVER_URL" - EnvToken = envNamespace + "TOKEN" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - ServerURL string - Token string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, 3600), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for ISPConfig 3 Dynamic DNS (DDNS) Module. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvServerURL, EnvToken) - if err != nil { - return nil, fmt.Errorf("ispconfig (DDNS module): %w", err) - } - - config := NewDefaultConfig() - config.ServerURL = values[EnvServerURL] - config.Token = values[EnvToken] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for ISPConfig 3 Dynamic DNS (DDNS) Module. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("ispconfig (DDNS module): the configuration of the DNS provider is nil") - } - - if config.ServerURL == "" { - return nil, errors.New("ispconfig (DDNS module): missing server URL") - } - - if config.Token == "" { - return nil, errors.New("ispconfig (DDNS module): missing token") - } - - client, err := internal.NewClient(config.ServerURL, config.Token) - if err != nil { - return nil, fmt.Errorf("ispconfig (DDNS module): %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to control checking compliance to spec. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -// Present creates a TXT record to fulfill the dns-01 challenge. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("ispconfig (DDNS module): could not find zone for domain %q: %w", domain, err) - } - - err = d.client.AddTXTRecord(context.Background(), dns01.UnFqdn(zone), info.EffectiveFQDN, info.Value) - if err != nil { - return fmt.Errorf("ispconfig (DDNS module): add record: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("ispconfig (DDNS module): could not find zone for domain %q: %w", domain, err) - } - - err = d.client.DeleteTXTRecord(context.Background(), dns01.UnFqdn(zone), info.EffectiveFQDN, info.Value) - if err != nil { - return fmt.Errorf("ispconfig (DDNS module): delete record: %w", err) - } - - return nil -} diff --git a/providers/dns/ispconfigddns/ispconfigddns.toml b/providers/dns/ispconfigddns/ispconfigddns.toml deleted file mode 100644 index 158ee9fbd..000000000 --- a/providers/dns/ispconfigddns/ispconfigddns.toml +++ /dev/null @@ -1,32 +0,0 @@ -Name = "ISPConfig 3 - Dynamic DNS (DDNS) Module" -Description = '''''' -URL = "https://www.ispconfig.org/" -Code = "ispconfigddns" -Since = "v4.31.0" - -Example = ''' -ISPCONFIG_DDNS_SERVER_URL="https://panel.example.com:8080" \ -ISPCONFIG_DDNS_TOKEN=xxxxxx \ -lego --dns ispconfigddns -d '*.example.com' -d example.com run -''' - -Additional = ''' -ISPConfig DNS provider supports leveraging the [ISPConfig 3 Dynamic DNS (DDNS) Module](https://github.com/mhofer117/ispconfig-ddns-module). - -Requires the DDNS module described at https://www.ispconfig.org/ispconfig/download/ - -See https://www.howtoforge.com/community/threads/ispconfig-3-danymic-dns-ddns-module.87967/ for additional details. -''' - -[Configuration] - [Configuration.Credentials] - ISPCONFIG_DDNS_SERVER_URL = "API server URL (ex: https://panel.example.com:8080)" - ISPCONFIG_DDNS_TOKEN = "DDNS API token" - [Configuration.Additional] - ISPCONFIG_DDNS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - ISPCONFIG_DDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - ISPCONFIG_DDNS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)" - ISPCONFIG_DDNS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://github.com/mhofer117/ispconfig-ddns-module/tree/master/lib/updater" diff --git a/providers/dns/ispconfigddns/ispconfigddns_test.go b/providers/dns/ispconfigddns/ispconfigddns_test.go deleted file mode 100644 index 58e7a8f54..000000000 --- a/providers/dns/ispconfigddns/ispconfigddns_test.go +++ /dev/null @@ -1,193 +0,0 @@ -package ispconfigddns - -import ( - "net/http/httptest" - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvServerURL, EnvToken). - WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvServerURL: "https://example.com", - EnvToken: "secret", - }, - }, - { - desc: "missing server URL", - envVars: map[string]string{ - EnvServerURL: "", - EnvToken: "secret", - }, - expected: "ispconfig (DDNS module): some credentials information are missing: ISPCONFIG_DDNS_SERVER_URL", - }, - { - desc: "missing token", - envVars: map[string]string{ - EnvServerURL: "https://example.com", - EnvToken: "", - }, - expected: "ispconfig (DDNS module): some credentials information are missing: ISPCONFIG_DDNS_TOKEN", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "ispconfig (DDNS module): some credentials information are missing: ISPCONFIG_DDNS_SERVER_URL,ISPCONFIG_DDNS_TOKEN", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - serverURL string - token string - expected string - }{ - { - desc: "success", - serverURL: "https://example.com", - token: "secret", - }, - { - desc: "missing server URL", - serverURL: "", - token: "secret", - expected: "ispconfig (DDNS module): missing server URL", - }, - { - desc: "missing token", - serverURL: "https://example.com", - token: "", - expected: "ispconfig (DDNS module): missing token", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.ServerURL = test.serverURL - config.Token = test.token - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.HTTPClient = server.Client() - config.Token = "secret" - config.ServerURL = server.URL - - return NewDNSProviderConfig(config) - }, - servermock.CheckHeader(). - WithBasicAuth("anonymous", "secret"), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("POST /ddns/update.php", - servermock.DumpRequest(), - servermock.CheckQueryParameter().Strict(). - With("action", "add"). - With("zone", "example.com"). - With("type", "TXT"). - With("record", "_acme-challenge.example.com."). - With("data", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"), - ). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("DELETE /ddns/update.php", - servermock.DumpRequest(), - servermock.CheckQueryParameter().Strict(). - With("action", "delete"). - With("zone", "example.com"). - With("type", "TXT"). - With("record", "_acme-challenge.example.com."). - With("data", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"), - ). - Build(t) - - err := provider.CleanUp("example.com", "abc", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/iwantmyname/internal/client.go b/providers/dns/iwantmyname/internal/client.go new file mode 100644 index 000000000..7a7c50e20 --- /dev/null +++ b/providers/dns/iwantmyname/internal/client.go @@ -0,0 +1,66 @@ +package internal + +import ( + "context" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + querystring "github.com/google/go-querystring/query" +) + +const defaultBaseURL = "https://iwantmyname.com/basicauth/ddns" + +// Client iwantmyname client. +type Client struct { + username string + password string + + baseURL *url.URL + HTTPClient *http.Client +} + +// NewClient creates a new Client. +func NewClient(username string, password string) *Client { + baseURL, _ := url.Parse(defaultBaseURL) + + return &Client{ + username: username, + password: password, + baseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + } +} + +// SendRequest send a request (create/add/delete) to the API. +func (c Client) SendRequest(ctx context.Context, record Record) error { + values, err := querystring.Values(record) + if err != nil { + return err + } + + endpoint := c.baseURL + endpoint.RawQuery = values.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), http.NoBody) + if err != nil { + return fmt.Errorf("unable to create request: %w", err) + } + + req.SetBasicAuth(c.username, c.password) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + return errutils.NewUnexpectedResponseStatusCodeError(req, resp) + } + + return nil +} diff --git a/providers/dns/iwantmyname/internal/client_test.go b/providers/dns/iwantmyname/internal/client_test.go new file mode 100644 index 000000000..b26f7c0f0 --- /dev/null +++ b/providers/dns/iwantmyname/internal/client_test.go @@ -0,0 +1,87 @@ +package internal + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/require" +) + +func checkParameter(query url.Values, key, expected string) error { + if query.Get(key) != expected { + return fmt.Errorf("%s: want %s got %s", key, expected, query.Get(key)) + } + return nil +} + +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient("user", "secret") + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client, mux +} + +func TestClient_Do(t *testing.T) { + client, mux := setupTest(t) + + mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + username, password, ok := req.BasicAuth() + if !ok { + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + if username != "user" { + http.Error(rw, fmt.Sprintf("username: want %s got %s", username, "user"), http.StatusUnauthorized) + return + } + + if password != "secret" { + http.Error(rw, fmt.Sprintf("password: want %s got %s", password, "secret"), http.StatusUnauthorized) + return + } + + query := req.URL.Query() + + values := map[string]string{ + "hostname": "example.com", + "type": "TXT", + "value": "data", + "ttl": "120", + } + + for k, v := range values { + err := checkParameter(query, k, v) + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + } + }) + + record := Record{ + Hostname: "example.com", + Type: "TXT", + Value: "data", + TTL: 120, + } + + err := client.SendRequest(context.Background(), record) + require.NoError(t, err) +} diff --git a/providers/dns/iwantmyname/internal/types.go b/providers/dns/iwantmyname/internal/types.go new file mode 100644 index 000000000..b259235f5 --- /dev/null +++ b/providers/dns/iwantmyname/internal/types.go @@ -0,0 +1,9 @@ +package internal + +// Record represents a record. +type Record struct { + Hostname string `url:"hostname,omitempty"` + Type string `url:"type,omitempty"` + Value string `url:"value,omitempty"` + TTL int `url:"ttl,omitempty"` +} diff --git a/providers/dns/iwantmyname/iwantmyname.go b/providers/dns/iwantmyname/iwantmyname.go index f53287e69..e828446ab 100644 --- a/providers/dns/iwantmyname/iwantmyname.go +++ b/providers/dns/iwantmyname/iwantmyname.go @@ -2,13 +2,15 @@ package iwantmyname import ( + "context" "errors" "fmt" "net/http" "time" - "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" + "github.com/go-acme/lego/v4/providers/dns/iwantmyname/internal" ) // Environment variables names. @@ -24,8 +26,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Username string @@ -38,12 +38,20 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { - return &Config{} + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config + client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for iwantmyname. @@ -63,7 +71,24 @@ func NewDNSProvider() (*DNSProvider, error) { // NewDNSProviderConfig return a DNSProvider instance configured for iwantmyname. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - return nil, errors.New("iwantmyname: the iwantmyname API has shut down https://github.com/go-acme/lego/issues/2563") + if config == nil { + return nil, errors.New("iwantmyname: the configuration of the DNS provider is nil") + } + + if config.Username == "" || config.Password == "" { + return nil, errors.New("iwantmyname: credentials missing") + } + + client := internal.NewClient(config.Username, config.Password) + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + return &DNSProvider{ + config: config, + client: client, + }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. @@ -74,10 +99,38 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, _, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + record := internal.Record{ + Hostname: dns01.UnFqdn(info.EffectiveFQDN), + Type: "TXT", + Value: info.Value, + TTL: d.config.TTL, + } + + err := d.client.SendRequest(context.Background(), record) + if err != nil { + return fmt.Errorf("iwantmyname: %w", err) + } + return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + record := internal.Record{ + Hostname: dns01.UnFqdn(info.EffectiveFQDN), + Type: "TXT", + Value: "delete", + TTL: d.config.TTL, + } + + err := d.client.SendRequest(context.Background(), record) + if err != nil { + return fmt.Errorf("iwantmyname: %w", err) + } + return nil } diff --git a/providers/dns/iwantmyname/iwantmyname.toml b/providers/dns/iwantmyname/iwantmyname.toml index a82c2b749..1bdf589be 100644 --- a/providers/dns/iwantmyname/iwantmyname.toml +++ b/providers/dns/iwantmyname/iwantmyname.toml @@ -1,9 +1,5 @@ -Name = "iwantmyname (Deprecated)" -Description = ''' -The iwantmyname API has shut down. - -https://github.com/go-acme/lego/issues/2563 -''' +Name = "iwantmyname" +Description = '''''' URL = "https://iwantmyname.com" Code = "iwantmyname" Since = "v4.7.0" @@ -11,7 +7,7 @@ Since = "v4.7.0" Example = ''' IWANTMYNAME_USERNAME=xxxxxxxx \ IWANTMYNAME_PASSWORD=xxxxxxxx \ -lego --dns iwantmyname -d '*.example.com' -d example.com run +lego --email you@example.com --dns iwantmyname --domains my.example.org run ''' [Configuration] @@ -19,10 +15,10 @@ lego --dns iwantmyname -d '*.example.com' -d example.com run IWANTMYNAME_USERNAME = "API username" IWANTMYNAME_PASSWORD = "API password" [Configuration.Additional] - IWANTMYNAME_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - IWANTMYNAME_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - IWANTMYNAME_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - IWANTMYNAME_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + IWANTMYNAME_POLLING_INTERVAL = "Time between DNS propagation check" + IWANTMYNAME_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + IWANTMYNAME_TTL = "The TTL of the TXT record used for the DNS challenge" + IWANTMYNAME_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://iwantmyname.com/developer/domain-dns-api" diff --git a/providers/dns/bookmyname/bookmyname_test.go b/providers/dns/iwantmyname/iwantmyname_test.go similarity index 80% rename from providers/dns/bookmyname/bookmyname_test.go rename to providers/dns/iwantmyname/iwantmyname_test.go index 8b3fa21e6..7ae4545b2 100644 --- a/providers/dns/bookmyname/bookmyname_test.go +++ b/providers/dns/iwantmyname/iwantmyname_test.go @@ -1,4 +1,4 @@ -package bookmyname +package iwantmyname import ( "testing" @@ -9,7 +9,8 @@ import ( const envDomain = envNamespace + "DOMAIN" -var envTest = tester.NewEnvTest(EnvUsername, EnvPassword).WithDomain(envDomain) +var envTest = tester.NewEnvTest(EnvUsername, EnvPassword). + WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { @@ -24,33 +25,30 @@ func TestNewDNSProvider(t *testing.T) { EnvPassword: "secret", }, }, - { - desc: "missing username", - envVars: map[string]string{ - EnvUsername: "", - EnvPassword: "secret", - }, - expected: "bookmyname: some credentials information are missing: BOOKMYNAME_USERNAME", - }, - { - desc: "missing paswword", - envVars: map[string]string{ - EnvUsername: "user", - EnvPassword: "", - }, - expected: "bookmyname: some credentials information are missing: BOOKMYNAME_PASSWORD", - }, { desc: "missing credentials", envVars: map[string]string{}, - expected: "bookmyname: some credentials information are missing: BOOKMYNAME_USERNAME,BOOKMYNAME_PASSWORD", + expected: "iwantmyname: some credentials information are missing: IWANTMYNAME_USERNAME,IWANTMYNAME_PASSWORD", + }, + { + desc: "missing username", + envVars: map[string]string{ + EnvPassword: "secret", + }, + expected: "iwantmyname: some credentials information are missing: IWANTMYNAME_USERNAME", + }, + { + desc: "missing password", + envVars: map[string]string{ + EnvUsername: "user", + }, + expected: "iwantmyname: some credentials information are missing: IWANTMYNAME_PASSWORD", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -81,19 +79,19 @@ func TestNewDNSProviderConfig(t *testing.T) { username: "user", password: "secret", }, + { + desc: "missing credentials", + expected: "iwantmyname: credentials missing", + }, { desc: "missing username", password: "secret", - expected: "bookmyname: credentials missing", + expected: "iwantmyname: credentials missing", }, { desc: "missing password", username: "user", - expected: "bookmyname: credentials missing", - }, - { - desc: "missing credentials", - expected: "bookmyname: credentials missing", + expected: "iwantmyname: credentials missing", }, } @@ -123,7 +121,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -137,7 +134,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/jdcloud/fixtures/create_record-request.json b/providers/dns/jdcloud/fixtures/create_record-request.json deleted file mode 100644 index 581c00fea..000000000 --- a/providers/dns/jdcloud/fixtures/create_record-request.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "domainId": "20", - "regionId": "cn-north-1", - "req": { - "hostRecord": "_acme-challenge", - "hostValue": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "jcloudRes": null, - "mxPriority": null, - "port": null, - "ttl": 120, - "type": "TXT", - "viewValue": -1, - "weight": null - } -} diff --git a/providers/dns/jdcloud/fixtures/create_record.json b/providers/dns/jdcloud/fixtures/create_record.json deleted file mode 100644 index 08bd3db26..000000000 --- a/providers/dns/jdcloud/fixtures/create_record.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "requestId": "azerty", - "error": { - "code": 0, - "status": "", - "message": "" - }, - "result": { - "dataList": { - "id": 123, - "hostRecord": "_acme-challenge", - "hostValue": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "jcloudRes": false, - "mxPriority": 0, - "port": 0, - "ttl": 120, - "type": "TXT", - "weight": 0, - "viewValue": [ - 1, - 2 - ] - } - } -} diff --git a/providers/dns/jdcloud/fixtures/delete_record.json b/providers/dns/jdcloud/fixtures/delete_record.json deleted file mode 100644 index 20525751c..000000000 --- a/providers/dns/jdcloud/fixtures/delete_record.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "requestId": "azerty", - "error": { - "code": 0, - "status": "", - "message": "" - }, - "result": {} -} diff --git a/providers/dns/jdcloud/fixtures/describe_domains_page1.json b/providers/dns/jdcloud/fixtures/describe_domains_page1.json deleted file mode 100644 index cde6dcd6f..000000000 --- a/providers/dns/jdcloud/fixtures/describe_domains_page1.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "requestId": "azerty", - "error": { - "code": 0, - "status": "", - "message": "" - }, - "result": { - "dataList": [ - { - "id": 1, - "domainName": "1.example" - }, - { - "id": 2, - "domainName": "2.example" - }, - { - "id": 3, - "domainName": "3.example" - }, - { - "id": 4, - "domainName": "4.example" - }, - { - "id": 5, - "domainName": "5.example" - }, - { - "id": 6, - "domainName": "6.example" - }, - { - "id": 7, - "domainName": "7.example" - }, - { - "id": 8, - "domainName": "8.example" - }, - { - "id": 9, - "domainName": "9.example" - }, - { - "id": 10, - "domainName": "10.example" - } - ], - "currentCount": 10, - "totalCount": 20, - "totalPage": 2 - } -} diff --git a/providers/dns/jdcloud/fixtures/describe_domains_page2.json b/providers/dns/jdcloud/fixtures/describe_domains_page2.json deleted file mode 100644 index b1e1560ab..000000000 --- a/providers/dns/jdcloud/fixtures/describe_domains_page2.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "requestId": "azerty", - "error": { - "code": 0, - "status": "", - "message": "" - }, - "result": { - "dataList": [ - { - "id": 11, - "domainName": "11.example" - }, - { - "id": 12, - "domainName": "12.example" - }, - { - "id": 13, - "domainName": "13.example" - }, - { - "id": 14, - "domainName": "14.example" - }, - { - "id": 15, - "domainName": "15.example" - }, - { - "id": 16, - "domainName": "16.example" - }, - { - "id": 17, - "domainName": "17.example" - }, - { - "id": 18, - "domainName": "18.example" - }, - { - "id": 19, - "domainName": "19.example" - }, - { - "id": 20, - "domainName": "example.com" - } - ], - "currentCount": 10, - "totalCount": 20, - "totalPage": 2 - } -} diff --git a/providers/dns/jdcloud/jdcloud.go b/providers/dns/jdcloud/jdcloud.go deleted file mode 100644 index 7d9ad4e6b..000000000 --- a/providers/dns/jdcloud/jdcloud.go +++ /dev/null @@ -1,217 +0,0 @@ -// Package jdcloud implements a DNS provider for solving the DNS-01 challenge using JD Cloud. -package jdcloud - -import ( - "errors" - "fmt" - "strconv" - "sync" - "time" - - "github.com/go-acme/jdcloud-sdk-go/core" - "github.com/go-acme/jdcloud-sdk-go/services/domainservice/apis" - jdcclient "github.com/go-acme/jdcloud-sdk-go/services/domainservice/client" - domainservice "github.com/go-acme/jdcloud-sdk-go/services/domainservice/models" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" -) - -// Environment variables names. -const ( - envNamespace = "JDCLOUD_" - - EnvAccessKeyID = envNamespace + "ACCESS_KEY_ID" - EnvAccessKeySecret = envNamespace + "ACCESS_KEY_SECRET" - EnvRegionID = envNamespace + "REGION_ID" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - AccessKeyID string - AccessKeySecret string - RegionID string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPTimeout time.Duration -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *jdcclient.DomainserviceClient - - recordIDs map[string]int - domainIDs map[string]int - recordIDsMu sync.Mutex -} - -// NewDNSProvider returns a DNSProvider instance configured for JD Cloud. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAccessKeyID, EnvAccessKeySecret) - if err != nil { - return nil, fmt.Errorf("jdcloud: %w", err) - } - - config := NewDefaultConfig() - config.AccessKeyID = values[EnvAccessKeyID] - config.AccessKeySecret = values[EnvAccessKeySecret] - - // https://docs.jdcloud.com/en/common-declaration/api/introduction#Region%20Code - config.RegionID = env.GetOrDefaultString(EnvRegionID, "cn-north-1") - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for JD Cloud. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("jdcloud: the configuration of the DNS provider is nil") - } - - if config.AccessKeyID == "" || config.AccessKeySecret == "" { - return nil, errors.New("jdcloud: missing credentials") - } - - cred := core.NewCredentials(config.AccessKeyID, config.AccessKeySecret) - - client := jdcclient.NewDomainserviceClient(cred) - client.DisableLogger() - client.Config.SetTimeout(config.HTTPTimeout) - - return &DNSProvider{ - config: config, - client: client, - recordIDs: make(map[string]int), - domainIDs: make(map[string]int), - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("jdcloud: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("jdcloud: %w", err) - } - - zone, err := d.findZone(dns01.UnFqdn(authZone)) - if err != nil { - return fmt.Errorf("jdcloud: %w", err) - } - - // https://docs.jdcloud.com/cn/jd-cloud-dns/api/createresourcerecord - crrr := apis.NewCreateResourceRecordRequestWithAllParams( - d.config.RegionID, - strconv.Itoa(zone.Id), - &domainservice.AddRR{ - HostRecord: subDomain, - HostValue: info.Value, - Ttl: d.config.TTL, - Type: "TXT", - ViewValue: -1, - }, - ) - - record, err := jdcclient.CreateResourceRecord(d.client, crrr) - if err != nil { - return fmt.Errorf("jdcloud: create resource record: %w", err) - } - - d.recordIDsMu.Lock() - d.domainIDs[token] = zone.Id - d.recordIDs[token] = record.Result.DataList.Id - d.recordIDsMu.Unlock() - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - d.recordIDsMu.Lock() - recordID, recordOK := d.recordIDs[token] - domainID, domainOK := d.domainIDs[token] - d.recordIDsMu.Unlock() - - if !recordOK { - return fmt.Errorf("jdcloud: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) - } - - if !domainOK { - return fmt.Errorf("jdcloud: unknown domain ID for '%s' '%s'", info.EffectiveFQDN, token) - } - - // https://docs.jdcloud.com/cn/jd-cloud-dns/api/deleteresourcerecord - drrr := apis.NewDeleteResourceRecordRequestWithAllParams( - d.config.RegionID, - strconv.Itoa(domainID), - strconv.Itoa(recordID), - ) - - _, err := jdcclient.DeleteResourceRecord(d.client, drrr) - if err != nil { - return fmt.Errorf("jdcloud: delete resource record: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -func (d *DNSProvider) findZone(zone string) (*domainservice.DomainInfo, error) { - // https://docs.jdcloud.com/cn/jd-cloud-dns/api/describedomains - ddr := apis.NewDescribeDomainsRequestWithoutParam() - ddr.SetRegionId(d.config.RegionID) - ddr.SetPageNumber(1) - ddr.SetPageSize(10) - ddr.SetDomainName(zone) - - for { - response, err := jdcclient.DescribeDomains(d.client, ddr) - if err != nil { - return nil, fmt.Errorf("describe domains: %w", err) - } - - for _, d := range response.Result.DataList { - if d.DomainName == zone { - return &d, nil - } - } - - if len(response.Result.DataList) < ddr.PageSize || response.Result.TotalPage <= ddr.PageNumber { - break - } - - ddr.SetPageNumber(ddr.PageNumber + 1) - } - - return nil, errors.New("zone not found") -} diff --git a/providers/dns/jdcloud/jdcloud.toml b/providers/dns/jdcloud/jdcloud.toml deleted file mode 100644 index 7ab403822..000000000 --- a/providers/dns/jdcloud/jdcloud.toml +++ /dev/null @@ -1,27 +0,0 @@ -Name = "JD Cloud" -Description = '''''' -URL = "https://www.jdcloud.com/" -Code = "jdcloud" -Since = "v4.31.0" - -Example = ''' -JDCLOUD_ACCESS_KEY_ID="xxx" \ -JDCLOUD_ACCESS_KEY_SECRET="yyy" \ -lego --dns jdcloud -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - JDCLOUD_ACCESS_KEY_ID = "Access key ID" - JDCLOUD_ACCESS_KEY_SECRET = "Access key secret" - [Configuration.Additional] - JDCLOUD_REGION_ID = "Region ID (Default: cn-north-1)" - JDCLOUD_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - JDCLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - JDCLOUD_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - JDCLOUD_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://docs.jdcloud.com/cn/jd-cloud-dns/api/overview" - Common = "https://docs.jdcloud.com/en/common-declaration/api/introduction" - GoClient = "https://github.com/jdcloud-api/jdcloud-sdk-go" diff --git a/providers/dns/jdcloud/jdcloud_test.go b/providers/dns/jdcloud/jdcloud_test.go deleted file mode 100644 index 6b3368938..000000000 --- a/providers/dns/jdcloud/jdcloud_test.go +++ /dev/null @@ -1,242 +0,0 @@ -package jdcloud - -import ( - "fmt" - "net" - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest( - EnvAccessKeyID, - EnvAccessKeySecret, - EnvRegionID, -).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAccessKeyID: "abc123", - EnvAccessKeySecret: "secret", - }, - }, - { - desc: "missing access key ID", - envVars: map[string]string{ - EnvAccessKeyID: "", - EnvAccessKeySecret: "secret", - }, - expected: "jdcloud: some credentials information are missing: JDCLOUD_ACCESS_KEY_ID", - }, - { - desc: "missing access key secret", - envVars: map[string]string{ - EnvAccessKeyID: "abc123", - EnvAccessKeySecret: "", - }, - expected: "jdcloud: some credentials information are missing: JDCLOUD_ACCESS_KEY_SECRET", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "jdcloud: some credentials information are missing: JDCLOUD_ACCESS_KEY_ID,JDCLOUD_ACCESS_KEY_SECRET", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - accessKeyID string - accessKeySecret string - expected string - }{ - { - desc: "success", - accessKeyID: "abc123", - accessKeySecret: "secret", - }, - { - desc: "missing access key ID", - accessKeySecret: "secret", - expected: "jdcloud: missing credentials", - }, - { - desc: "missing access key secret", - accessKeyID: "abc123", - expected: "jdcloud: missing credentials", - }, - { - desc: "missing credentials", - expected: "jdcloud: missing credentials", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.AccessKeyID = test.accessKeyID - config.AccessKeySecret = test.accessKeySecret - config.RegionID = "cn-north-1" - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.AccessKeyID = "abc123" - config.AccessKeySecret = "secret" - config.RegionID = "cn-north-1" - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - serverURL, _ := url.Parse(server.URL) - - p.client.Config.SetEndpoint(net.JoinHostPort(serverURL.Hostname(), serverURL.Port())) - p.client.Config.SetScheme(serverURL.Scheme) - p.client.Config.SetTimeout(server.Client().Timeout) - - return p, nil - }, - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("GET /v2/regions/cn-north-1/domain", - http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - pageNumber := req.URL.Query().Get("pageNumber") - - servermock.ResponseFromFixture( - fmt.Sprintf("describe_domains_page%s.json", pageNumber), - ).ServeHTTP(rw, req) - }), - servermock.CheckQueryParameter().Strict(). - With("domainName", "example.com"). - WithRegexp("pageNumber", `(1|2)`). - With("pageSize", "10"), - servermock.CheckHeader(). - WithRegexp("Authorization", - `JDCLOUD2-HMAC-SHA256 Credential=abc123/\d{8}/cn-north-1/domainservice/jdcloud2_request, SignedHeaders=content-type;host;x-jdcloud-date;x-jdcloud-nonce, Signature=\w+`). - WithRegexp("X-Jdcloud-Date", `\d{8}T\d{6}Z`). - WithRegexp("X-Jdcloud-Nonce", `[\w-]+`), - ). - Route("POST /v2/regions/cn-north-1/domain/20/ResourceRecord", - servermock.ResponseFromFixture("create_record.json"), - servermock.CheckRequestJSONBodyFromFixture("create_record-request.json"), - servermock.CheckHeader(). - WithRegexp("Authorization", - `JDCLOUD2-HMAC-SHA256 Credential=abc123/\d{8}/cn-north-1/domainservice/jdcloud2_request, SignedHeaders=content-type;host;x-jdcloud-date;x-jdcloud-nonce, Signature=\w+`). - WithRegexp("X-Jdcloud-Date", `\d{8}T\d{6}Z`). - WithRegexp("X-Jdcloud-Nonce", `[\w-]+`), - ). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) - - require.Len(t, provider.domainIDs, 1) - require.Len(t, provider.recordIDs, 1) - - assert.Equal(t, 20, provider.domainIDs["abc"]) - assert.Equal(t, 123, provider.recordIDs["abc"]) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("DELETE /v2/regions/cn-north-1/domain/20/ResourceRecord/123", - servermock.ResponseFromFixture("delete_record.json"), - servermock.CheckHeader(). - WithRegexp("Authorization", - `JDCLOUD2-HMAC-SHA256 Credential=abc123/\d{8}/cn-north-1/domainservice/jdcloud2_request, SignedHeaders=content-type;host;x-jdcloud-date;x-jdcloud-nonce, Signature=\w+`). - WithRegexp("X-Jdcloud-Date", `\d{8}T\d{6}Z`). - WithRegexp("X-Jdcloud-Nonce", `[\w-]+`), - ). - Build(t) - - provider.domainIDs["abc"] = 20 - provider.recordIDs["abc"] = 123 - - err := provider.CleanUp("example.com", "abc", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/joker/internal/dmapi/client.go b/providers/dns/joker/internal/dmapi/client.go index 576410723..04f4350a9 100644 --- a/providers/dns/joker/internal/dmapi/client.go +++ b/providers/dns/joker/internal/dmapi/client.go @@ -126,7 +126,7 @@ func parseResponse(message string) *Response { lines, body, _ := strings.Cut(message, "\n\n") - for line := range strings.Lines(lines) { + for _, line := range strings.Split(lines, "\n") { if strings.TrimSpace(line) == "" { continue } @@ -176,15 +176,12 @@ func RemoveTxtEntryFromZone(zone, relative string) (string, bool) { prefix := fmt.Sprintf("%s TXT 0 ", relative) modified := false - var zoneEntries []string - - for line := range strings.Lines(zone) { + for _, line := range strings.Split(zone, "\n") { if strings.HasPrefix(line, prefix) { modified = true continue } - zoneEntries = append(zoneEntries, line) } @@ -195,7 +192,7 @@ func RemoveTxtEntryFromZone(zone, relative string) (string, bool) { func AddTxtEntryToZone(zone, relative, value string, ttl int) string { var zoneEntries []string - for line := range strings.Lines(zone) { + for _, line := range strings.Split(zone, "\n") { zoneEntries = append(zoneEntries, fixTxtLines(line)) } diff --git a/providers/dns/joker/internal/dmapi/client_test.go b/providers/dns/joker/internal/dmapi/client_test.go index 5b6d68740..dc6653bf0 100644 --- a/providers/dns/joker/internal/dmapi/client_test.go +++ b/providers/dns/joker/internal/dmapi/client_test.go @@ -7,7 +7,6 @@ import ( "net/url" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -24,17 +23,14 @@ const ( serverErrorUsername = "error" ) -func mockBuilder(auth AuthInfo) *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient(auth) - client.BaseURL = server.URL - client.HTTPClient = server.Client() +func setupTest(t *testing.T) (*http.ServeMux, string) { + t.Helper() - return client, nil - }, - servermock.CheckHeader(). - WithContentTypeFromURLEncoded()) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + return mux, server.URL } func TestClient_GetZone(t *testing.T) { @@ -74,25 +70,30 @@ func TestClient_GetZone(t *testing.T) { }, } - client := mockBuilder(AuthInfo{APIKey: "12345"}). - Route("POST /dns-zone-get", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - authSid := req.FormValue("auth-sid") - domain := req.FormValue("domain") + mux, serverURL := setupTest(t) - switch { - case authSid == correctAPIKey && domain == "known": - _, _ = io.WriteString(rw, "Status-Code: 0\nStatus-Text: OK\n\n"+testZone) - case authSid == incorrectAPIKey || (authSid == correctAPIKey && domain == "unknown"): - _, _ = io.WriteString(rw, "Status-Code: 2202\nStatus-Text: Authorization error") - default: - http.NotFound(rw, req) - } - })). - Build(t) + mux.HandleFunc("/dns-zone-get", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + authSid := r.FormValue("auth-sid") + domain := r.FormValue("domain") + + switch { + case authSid == correctAPIKey && domain == "known": + _, _ = io.WriteString(w, "Status-Code: 0\nStatus-Text: OK\n\n"+testZone) + case authSid == incorrectAPIKey || (authSid == correctAPIKey && domain == "unknown"): + _, _ = io.WriteString(w, "Status-Code: 2202\nStatus-Text: Authorization error") + default: + http.NotFound(w, r) + } + }) for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - response, err := client.GetZone(mockContext(t, test.authSid), test.domain) + client := NewClient(AuthInfo{APIKey: "12345"}) + client.BaseURL = serverURL + + response, err := client.GetZone(mockContext(test.authSid), test.domain) if test.expectedError { require.Error(t, err) } else { diff --git a/providers/dns/joker/internal/dmapi/identity.go b/providers/dns/joker/internal/dmapi/identity.go index 63c0b2ea1..351d987e9 100644 --- a/providers/dns/joker/internal/dmapi/identity.go +++ b/providers/dns/joker/internal/dmapi/identity.go @@ -24,7 +24,6 @@ type Token struct { // login performs a log in to Joker's DMAPI. func (c *Client) login(ctx context.Context) (*Response, error) { var values url.Values - switch { case c.username != "" && c.password != "": values = url.Values{ @@ -107,6 +106,5 @@ func formatResponseError(response *Response, err error) error { if response != nil { return fmt.Errorf("joker: DMAPI error: %w Response: %v", err, response.Headers) } - return fmt.Errorf("joker: DMAPI error: %w", err) } diff --git a/providers/dns/joker/internal/dmapi/identity_test.go b/providers/dns/joker/internal/dmapi/identity_test.go index d2a80f2e6..418deaf4f 100644 --- a/providers/dns/joker/internal/dmapi/identity_test.go +++ b/providers/dns/joker/internal/dmapi/identity_test.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "net/http/httptest" "sync/atomic" "testing" "time" @@ -13,14 +14,12 @@ import ( "github.com/stretchr/testify/require" ) -func mockContext(t *testing.T, sessionID string) context.Context { - t.Helper() - +func mockContext(sessionID string) context.Context { if sessionID == "" { sessionID = "xxx" } - return context.WithValue(t.Context(), sessionIDKey, sessionID) + return context.WithValue(context.Background(), sessionIDKey, sessionID) } func TestClient_login_apikey(t *testing.T) { @@ -57,24 +56,29 @@ func TestClient_login_apikey(t *testing.T) { }, } + mux, serverURL := setupTest(t) + + mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + switch r.FormValue("api-key") { + case correctAPIKey: + _, _ = io.WriteString(w, "Status-Code: 0\nStatus-Text: OK\nAuth-Sid: 123\n\ncom\nnet") + case incorrectAPIKey: + _, _ = io.WriteString(w, "Status-Code: 2200\nStatus-Text: Authentication error") + case serverErrorAPIKey: + http.NotFound(w, r) + default: + _, _ = io.WriteString(w, "Status-Code: 2202\nStatus-Text: OK\n\ncom\nnet") + } + }) + for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - client := mockBuilder(AuthInfo{APIKey: test.apiKey}). - Route("POST /login", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - switch req.FormValue("api-key") { - case correctAPIKey: - _, _ = io.WriteString(rw, "Status-Code: 0\nStatus-Text: OK\nAuth-Sid: 123\n\ncom\nnet") - case incorrectAPIKey: - _, _ = io.WriteString(rw, "Status-Code: 2200\nStatus-Text: Authentication error") - case serverErrorAPIKey: - http.NotFound(rw, req) - default: - _, _ = io.WriteString(rw, "Status-Code: 2202\nStatus-Text: OK\n\ncom\nnet") - } - })). - Build(t) + client := NewClient(AuthInfo{APIKey: test.apiKey}) + client.BaseURL = serverURL - response, err := client.login(t.Context()) + response, err := client.login(context.Background()) if test.expectedError { require.Error(t, err) } else { @@ -127,24 +131,29 @@ func TestClient_login_username(t *testing.T) { }, } + mux, serverURL := setupTest(t) + + mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + switch r.FormValue("username") { + case correctUsername: + _, _ = io.WriteString(w, "Status-Code: 0\nStatus-Text: OK\nAuth-Sid: 123\n\ncom\nnet") + case incorrectUsername: + _, _ = io.WriteString(w, "Status-Code: 2200\nStatus-Text: Authentication error") + case serverErrorUsername: + http.NotFound(w, r) + default: + _, _ = io.WriteString(w, "Status-Code: 2202\nStatus-Text: OK\n\ncom\nnet") + } + }) + for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - client := mockBuilder(AuthInfo{Username: test.username, Password: test.password}). - Route("POST /login", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - switch req.FormValue("username") { - case correctUsername: - _, _ = io.WriteString(rw, "Status-Code: 0\nStatus-Text: OK\nAuth-Sid: 123\n\ncom\nnet") - case incorrectUsername: - _, _ = io.WriteString(rw, "Status-Code: 2200\nStatus-Text: Authentication error") - case serverErrorUsername: - http.NotFound(rw, req) - default: - _, _ = io.WriteString(rw, "Status-Code: 2202\nStatus-Text: OK\n\ncom\nnet") - } - })). - Build(t) + client := NewClient(AuthInfo{Username: test.username, Password: test.password}) + client.BaseURL = serverURL - response, err := client.login(t.Context()) + response, err := client.login(context.Background()) if test.expectedError { require.Error(t, err) } else { @@ -186,24 +195,28 @@ func TestClient_logout(t *testing.T) { }, } + mux, serverURL := setupTest(t) + + mux.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + switch r.FormValue("auth-sid") { + case correctAPIKey: + _, _ = io.WriteString(w, "Status-Code: 0\nStatus-Text: OK\n") + case incorrectAPIKey: + _, _ = io.WriteString(w, "Status-Code: 2200\nStatus-Text: Authentication error") + default: + http.NotFound(w, r) + } + }) + for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - client := mockBuilder(AuthInfo{APIKey: "12345"}). - Route("POST /logout", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - switch req.FormValue("auth-sid") { - case correctAPIKey: - _, _ = io.WriteString(rw, "Status-Code: 0\nStatus-Text: OK\n") - case incorrectAPIKey: - _, _ = io.WriteString(rw, "Status-Code: 2200\nStatus-Text: Authentication error") - default: - http.NotFound(rw, req) - } - })). - Build(t) - + client := NewClient(AuthInfo{APIKey: "12345"}) + client.BaseURL = serverURL client.token = &Token{SessionID: test.authSid} - response, err := client.Logout(mockContext(t, test.authSid)) + response, err := client.Logout(mockContext(test.authSid)) if test.expectedError { require.Error(t, err) } else { @@ -216,23 +229,31 @@ func TestClient_logout(t *testing.T) { } func TestClient_CreateAuthenticatedContext(t *testing.T) { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + id := atomic.Int32{} id.Add(100) - client := mockBuilder(AuthInfo{Username: correctUsername, Password: "secret"}). - Route("POST /login", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - switch req.FormValue("username") { - case correctUsername: - _, _ = fmt.Fprintf(rw, "Status-Code: 0\nStatus-Text: OK\nAuth-Sid: %d\n\ncom\nnet", id.Load()) - id.Add(100) + mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) - default: - _, _ = io.WriteString(rw, "Status-Code: 2200\nStatus-Text: Authentication error") - } - })). - Build(t) + switch r.FormValue("username") { + case correctUsername: + _, _ = fmt.Fprintf(w, "Status-Code: 0\nStatus-Text: OK\nAuth-Sid: %d\n\ncom\nnet", id.Load()) + id.Add(100) - ctx, err := client.CreateAuthenticatedContext(t.Context()) + default: + _, _ = io.WriteString(w, "Status-Code: 2200\nStatus-Text: Authentication error") + } + }) + + client := NewClient(AuthInfo{Username: correctUsername, Password: "secret"}) + client.HTTPClient = server.Client() + client.BaseURL = server.URL + + ctx, err := client.CreateAuthenticatedContext(context.Background()) require.NoError(t, err) assert.Equal(t, "100", getSessionID(ctx)) @@ -242,7 +263,7 @@ func TestClient_CreateAuthenticatedContext(t *testing.T) { client.token.SessionID = "cache" client.muToken.Unlock() - ctx, err = client.CreateAuthenticatedContext(t.Context()) + ctx, err = client.CreateAuthenticatedContext(context.Background()) require.NoError(t, err) assert.Equal(t, "cache", getSessionID(ctx)) @@ -252,7 +273,7 @@ func TestClient_CreateAuthenticatedContext(t *testing.T) { client.token.ExpireAt = time.Now().UTC().Add(-1 * time.Hour) client.muToken.Unlock() - ctx, err = client.CreateAuthenticatedContext(t.Context()) + ctx, err = client.CreateAuthenticatedContext(context.Background()) require.NoError(t, err) assert.Equal(t, "200", getSessionID(ctx)) diff --git a/providers/dns/joker/internal/svc/client_test.go b/providers/dns/joker/internal/svc/client_test.go index a6cb299e4..6803ae844 100644 --- a/providers/dns/joker/internal/svc/client_test.go +++ b/providers/dns/joker/internal/svc/client_test.go @@ -1,66 +1,88 @@ package svc import ( + "context" + "fmt" + "io" + "net/http" "net/http/httptest" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("test", "secret") - client.BaseURL = server.URL - client.HTTPClient = server.Client() +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() - return client, nil - }, - servermock.CheckHeader(). - WithContentTypeFromURLEncoded()) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient("test", "secret") + client.BaseURL = server.URL + client.HTTPClient = server.Client() + + return client, mux } func TestClient_Send(t *testing.T) { - client := mockBuilder(). - Route("POST /", - servermock.RawStringResponse("OK: 1 inserted, 0 deleted"), - servermock.CheckForm().Strict(). - With("zone", "example.com"). - With("label", "_acme-challenge"). - With("type", "TXT"). - With("value", "123"). - With("username", "test"). - With("password", "secret"), - ). - Build(t) + client, mux := setupTest(t) + + mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + all, _ := io.ReadAll(req.Body) + + if string(all) != "label=_acme-challenge&password=secret&type=TXT&username=test&value=123&zone=example.com" { + http.Error(rw, fmt.Sprintf("invalid request: %q", string(all)), http.StatusBadRequest) + return + } + + _, err := rw.Write([]byte("OK: 1 inserted, 0 deleted")) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) zone := "example.com" label := "_acme-challenge" value := "123" - err := client.SendRequest(t.Context(), zone, label, value) + err := client.SendRequest(context.Background(), zone, label, value) require.NoError(t, err) } func TestClient_Send_empty(t *testing.T) { - client := mockBuilder(). - Route("POST /", - servermock.RawStringResponse("OK: 1 inserted, 0 deleted"), - servermock.CheckForm().Strict(). - With("zone", "example.com"). - With("label", "_acme-challenge"). - With("type", "TXT"). - With("value", ""). - With("username", "test"). - With("password", "secret"), - ). - Build(t) + client, mux := setupTest(t) + + mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + all, _ := io.ReadAll(req.Body) + + if string(all) != "label=_acme-challenge&password=secret&type=TXT&username=test&value=&zone=example.com" { + http.Error(rw, fmt.Sprintf("invalid request: %q", string(all)), http.StatusBadRequest) + return + } + + _, err := rw.Write([]byte("OK: 1 inserted, 0 deleted")) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) zone := "example.com" label := "_acme-challenge" value := "" - err := client.SendRequest(t.Context(), zone, label, value) + err := client.SendRequest(context.Background(), zone, label, value) require.NoError(t, err) } diff --git a/providers/dns/joker/joker.toml b/providers/dns/joker/joker.toml index 20e481a6d..786097ac4 100644 --- a/providers/dns/joker/joker.toml +++ b/providers/dns/joker/joker.toml @@ -9,17 +9,17 @@ Example = ''' JOKER_API_MODE=SVC \ JOKER_USERNAME= \ JOKER_PASSWORD= \ -lego --dns joker -d '*.example.com' -d example.com run +lego --email you@example.com --dns joker --domains my.example.org run # DMAPI JOKER_API_MODE=DMAPI \ JOKER_USERNAME= \ JOKER_PASSWORD= \ -lego --dns joker -d '*.example.com' -d example.com run +lego --email you@example.com --dns joker --domains my.example.org run ## or JOKER_API_MODE=DMAPI \ JOKER_API_KEY= \ -lego --dns joker -d '*.example.com' -d example.com run +lego --email you@example.com --dns joker --domains my.example.org run ''' Additional = ''' @@ -48,11 +48,11 @@ As per [Joker.com documentation](https://joker.com/faq/content/6/496/en/let_s-en JOKER_PASSWORD = "Joker.com password" JOKER_API_KEY = "API key (only with DMAPI mode)" [Configuration.Additional] - JOKER_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - JOKER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - JOKER_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - JOKER_HTTP_TIMEOUT = "API request timeout in seconds (Default: 60)" - JOKER_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 60), only with 'SVC' mode" + JOKER_POLLING_INTERVAL = "Time between DNS propagation check" + JOKER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + JOKER_TTL = "The TTL of the TXT record used for the DNS challenge" + JOKER_HTTP_TIMEOUT = "API request timeout" + JOKER_SEQUENCE_INTERVAL = "Time between sequential requests (only with 'SVC' mode)" [Links] API = "https://joker.com/faq/category/39/22-dmapi.html" diff --git a/providers/dns/joker/joker_test.go b/providers/dns/joker/joker_test.go index bc21ccbbc..a71e4d9fe 100644 --- a/providers/dns/joker/joker_test.go +++ b/providers/dns/joker/joker_test.go @@ -20,7 +20,7 @@ func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string - expected any + expected interface{} }{ { desc: "mode DMAPI (default)", @@ -53,7 +53,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -73,7 +72,7 @@ func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string mode string - expected any + expected interface{} }{ { desc: "mode DMAPI (default)", @@ -113,7 +112,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -127,7 +125,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/joker/provider_dmapi.go b/providers/dns/joker/provider_dmapi.go index 11f850136..ec85d5705 100644 --- a/providers/dns/joker/provider_dmapi.go +++ b/providers/dns/joker/provider_dmapi.go @@ -6,16 +6,12 @@ import ( "fmt" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/joker/internal/dmapi" ) -var _ challenge.ProviderTimeout = (*dmapiProvider)(nil) - // dmapiProvider implements the challenge.Provider interface. type dmapiProvider struct { config *Config @@ -28,7 +24,6 @@ func newDmapiProvider() (*dmapiProvider, error) { values, err := env.Get(EnvAPIKey) if err != nil { var errU error - values, errU = env.Get(EnvUsername, EnvPassword) if errU != nil { //nolint:errorlint // false-positive @@ -68,8 +63,6 @@ func newDmapiProviderConfig(config *Config) (*dmapiProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &dmapiProvider{config: config, client: client}, nil } @@ -162,7 +155,6 @@ func (d *dmapiProvider) CleanUp(domain, token, keyAuth string) error { if err != nil { return formatResponseError(response, err) } - return nil } @@ -171,6 +163,5 @@ func formatResponseError(response *dmapi.Response, err error) error { if response != nil { return fmt.Errorf("joker: DMAPI error: %w Response: %v", err, response.Headers) } - return fmt.Errorf("joker: DMAPI error: %w", err) } diff --git a/providers/dns/joker/provider_dmapi_test.go b/providers/dns/joker/provider_dmapi_test.go index 06f283872..4704f2b80 100644 --- a/providers/dns/joker/provider_dmapi_test.go +++ b/providers/dns/joker/provider_dmapi_test.go @@ -58,7 +58,6 @@ func Test_newDmapiProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) diff --git a/providers/dns/joker/provider_svc.go b/providers/dns/joker/provider_svc.go index f4d8fcf3f..c9edfded1 100644 --- a/providers/dns/joker/provider_svc.go +++ b/providers/dns/joker/provider_svc.go @@ -6,15 +6,11 @@ import ( "fmt" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/joker/internal/svc" ) -var _ challenge.ProviderTimeout = (*svcProvider)(nil) - // svcProvider implements the challenge.Provider interface. type svcProvider struct { config *Config @@ -48,8 +44,6 @@ func newSvcProviderConfig(config *Config) (*svcProvider, error) { client := svc.NewClient(config.Username, config.Password) - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &svcProvider{config: config, client: client}, nil } diff --git a/providers/dns/joker/provider_svc_test.go b/providers/dns/joker/provider_svc_test.go index dc981b6b4..ad6c74c87 100644 --- a/providers/dns/joker/provider_svc_test.go +++ b/providers/dns/joker/provider_svc_test.go @@ -49,7 +49,6 @@ func Test_newSvcProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) diff --git a/providers/dns/keyhelp/internal/client.go b/providers/dns/keyhelp/internal/client.go deleted file mode 100644 index a5a80db5c..000000000 --- a/providers/dns/keyhelp/internal/client.go +++ /dev/null @@ -1,175 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" -) - -// APIKeyHeader API key header. -const APIKeyHeader = "X-Api-Key" - -// Client the KeyHelp API client. -type Client struct { - apiKey string - - baseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(baseURL, apiKey string) (*Client, error) { - if baseURL == "" { - return nil, errors.New("missing base URL") - } - - if apiKey == "" { - return nil, errors.New("credentials missing") - } - - base, err := url.Parse(baseURL) - if err != nil { - return nil, fmt.Errorf("parse base URL: %w", err) - } - - return &Client{ - apiKey: apiKey, - baseURL: base.JoinPath("api", "v2"), - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -func (c *Client) do(req *http.Request, result any) error { - req.Header.Set(APIKeyHeader, c.apiKey) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) { - endpoint := c.baseURL.JoinPath("domains") - - query := endpoint.Query() - query.Set("sort", "domain_utf8") - endpoint.RawQuery = query.Encode() - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var result []Domain - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return result, nil -} - -func (c *Client) ListDomainRecords(ctx context.Context, domainID int) (*DomainRecords, error) { - endpoint := c.baseURL.JoinPath("dns", strconv.Itoa(domainID)) - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var result DomainRecords - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return &result, nil -} - -func (c *Client) UpdateDomainRecords(ctx context.Context, domainID int, records DomainRecords) (*DomainID, error) { - endpoint := c.baseURL.JoinPath("dns", strconv.Itoa(domainID)) - - req, err := newJSONRequest(ctx, http.MethodPut, endpoint, records) - if err != nil { - return nil, err - } - - var result DomainID - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return &result, nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIError - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return &errAPI -} diff --git a/providers/dns/keyhelp/internal/client_test.go b/providers/dns/keyhelp/internal/client_test.go deleted file mode 100644 index 80b21495b..000000000 --- a/providers/dns/keyhelp/internal/client_test.go +++ /dev/null @@ -1,169 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient(server.URL, "secret") - if err != nil { - return nil, err - } - - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader(). - With(APIKeyHeader, "secret"). - WithJSONHeaders(), - ) -} - -func TestClient_ListDomains(t *testing.T) { - client := mockBuilder(). - Route("GET /api/v2/domains", - servermock.ResponseFromFixture("get_domains.json"), - servermock.CheckQueryParameter(). - With("sort", "domain_utf8"). - Strict()). - Build(t) - - domains, err := client.ListDomains(t.Context()) - require.NoError(t, err) - - expected := []Domain{{ - ID: 8, - UserID: 4, - ParentDomainID: 0, - Status: 1, - Domain: "example.com", - DomainUTF8: "example.com", - IsEmailDomain: true, - }} - - assert.Equal(t, expected, domains) -} - -func TestClient_ListDomains_error(t *testing.T) { - client := mockBuilder(). - Route("GET /api/v2/domains", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) - - _, err := client.ListDomains(t.Context()) - - require.EqualError(t, err, "401 Unauthorized: API key is missing or invalid.") -} - -func TestClient_ListDomainRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /api/v2/dns/123", - servermock.ResponseFromFixture("get_domain_records.json")). - Build(t) - - domainRecords, err := client.ListDomainRecords(t.Context(), 123) - require.NoError(t, err) - - expected := &DomainRecords{ - DkimRecord: `default._domainkey IN TXT ( "v=DKIM1; k=rsa; s=email; " "...DKIM KEY..." )`, - Records: &Records{ - Soa: &SOARecord{ - TTL: 86400, - PrimaryNs: "ns.example.com.", - RName: "root.example.com.", - Refresh: 14400, - Retry: 1800, - Expire: 604800, - Minimum: 3600, - }, - Other: []Record{{ - Host: "@", - TTL: 86400, - Type: "A", - Value: "192.168.178.1", - }}, - }, - } - - assert.Equal(t, expected, domainRecords) -} - -func TestClient_ListDomainRecords_error(t *testing.T) { - client := mockBuilder(). - Route("GET /api/v2/dns/8", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) - - _, err := client.ListDomainRecords(t.Context(), 8) - - require.EqualError(t, err, "401 Unauthorized: API key is missing or invalid.") -} - -func TestClient_UpdateDomainRecords(t *testing.T) { - client := mockBuilder(). - Route("PUT /api/v2/dns/8", - servermock.ResponseFromFixture("update_domain_records.json"), - servermock.CheckRequestJSONBodyFromFixture("update_domain_records-request.json")). - Build(t) - - records := DomainRecords{ - DkimRecord: `default._domainkey IN TXT ( "v=DKIM1; k=rsa; s=email; " "...DKIM KEY..." )`, - Records: &Records{ - Soa: &SOARecord{ - TTL: 86400, - PrimaryNs: "ns.example.com.", - RName: "root.example.com.", - Refresh: 14400, - Retry: 1800, - Expire: 604800, - Minimum: 3600, - }, - Other: []Record{ - { - Host: "@", - TTL: 86400, - Type: "A", - Value: "192.168.178.1", - }, - { - Host: "_acme-challenge", - TTL: 120, - Type: "TXT", - Value: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - }, - }, - }, - } - - domainID, err := client.UpdateDomainRecords(t.Context(), 8, records) - require.NoError(t, err) - - expected := &DomainID{ID: 8} - - assert.Equal(t, expected, domainID) -} - -func TestClient_UpdateDomainRecords_error(t *testing.T) { - client := mockBuilder(). - Route("PUT /api/v2/dns/123", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) - - records := DomainRecords{} - - _, err := client.UpdateDomainRecords(t.Context(), 123, records) - - require.EqualError(t, err, "401 Unauthorized: API key is missing or invalid.") -} diff --git a/providers/dns/keyhelp/internal/fixtures/error.json b/providers/dns/keyhelp/internal/fixtures/error.json deleted file mode 100644 index 4fdf5e8f5..000000000 --- a/providers/dns/keyhelp/internal/fixtures/error.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "code": "401 Unauthorized", - "message": "API key is missing or invalid." -} diff --git a/providers/dns/keyhelp/internal/fixtures/get_domain_records.json b/providers/dns/keyhelp/internal/fixtures/get_domain_records.json deleted file mode 100644 index 50483bb8e..000000000 --- a/providers/dns/keyhelp/internal/fixtures/get_domain_records.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "is_custom_dns": false, - "is_dns_disabled": false, - "dkim_record": "default._domainkey IN TXT ( \"v=DKIM1; k=rsa; s=email; \" \"...DKIM KEY...\" )", - "records": { - "soa": { - "ttl": 86400, - "primary_ns": "ns.example.com.", - "rname": "root.example.com.", - "refresh": 14400, - "retry": 1800, - "expire": 604800, - "minimum": 3600 - }, - "other": [ - { - "host": "@", - "ttl": 86400, - "type": "A", - "value": "192.168.178.1" - } - ] - } -} diff --git a/providers/dns/keyhelp/internal/fixtures/get_domain_records2.json b/providers/dns/keyhelp/internal/fixtures/get_domain_records2.json deleted file mode 100644 index cd49fd6d0..000000000 --- a/providers/dns/keyhelp/internal/fixtures/get_domain_records2.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "is_custom_dns": false, - "is_dns_disabled": false, - "dkim_record": "default._domainkey IN TXT ( \"v=DKIM1; k=rsa; s=email; \" \"...DKIM KEY...\" )", - "records": { - "soa": { - "ttl": 86400, - "primary_ns": "ns.example.com.", - "rname": "root.example.com.", - "refresh": 14400, - "retry": 1800, - "expire": 604800, - "minimum": 3600 - }, - "other": [ - { - "host": "@", - "ttl": 86400, - "type": "A", - "value": "192.168.178.1" - }, - { - "host": "_acme-challenge", - "ttl": 120, - "type": "TXT", - "value": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" - } - ] - } -} diff --git a/providers/dns/keyhelp/internal/fixtures/get_domains.json b/providers/dns/keyhelp/internal/fixtures/get_domains.json deleted file mode 100644 index 28ae0887d..000000000 --- a/providers/dns/keyhelp/internal/fixtures/get_domains.json +++ /dev/null @@ -1,41 +0,0 @@ -[ - { - "id": 8, - "id_user": 4, - "id_parent_domain": 0, - "status": 1, - "domain": "example.com", - "domain_utf8": "example.com", - "created_at": "2019-08-15T11:29:13+02:00", - "php_version": "", - "traffic": 32434624, - "is_disabled": false, - "delete_on": "2025-09-02T19:31:14+0000", - "dkim_selector": "default", - "dkim_record": "default._domainkey IN TXT ( \"v=DKIM1; k=rsa; s=email; \" \"...DKIM KEY...\" )", - "is_custom_dns": false, - "is_dns_disabled": false, - "is_subdomain": false, - "is_system_domain": false, - "is_email_domain": true, - "is_email_sending_only": false, - "target": { - "target": "https://www.keyhelp.de", - "is_forwarding": true, - "forwarding_type": 301 - }, - "security": { - "id_certificate": 0, - "lets_encrypt": true, - "is_prefer_https": true, - "is_hsts": true, - "hsts_max_age": 10368000, - "hsts_include": true, - "hsts_preload": true - }, - "apache": { - "http_directives": "# My custom HTTP directives", - "https_directives": "# My custom HTTPS directives" - } - } -] diff --git a/providers/dns/keyhelp/internal/fixtures/update_domain_records-request.json b/providers/dns/keyhelp/internal/fixtures/update_domain_records-request.json deleted file mode 100644 index 6f83ead11..000000000 --- a/providers/dns/keyhelp/internal/fixtures/update_domain_records-request.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "dkim_record": "default._domainkey IN TXT ( \"v=DKIM1; k=rsa; s=email; \" \"...DKIM KEY...\" )", - "records": { - "soa": { - "ttl": 86400, - "primary_ns": "ns.example.com.", - "rname": "root.example.com.", - "refresh": 14400, - "retry": 1800, - "expire": 604800, - "minimum": 3600 - }, - "other": [ - { - "host": "@", - "ttl": 86400, - "type": "A", - "value": "192.168.178.1" - }, - { - "host": "_acme-challenge", - "ttl": 120, - "type": "TXT", - "value": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" - } - ] - } -} diff --git a/providers/dns/keyhelp/internal/fixtures/update_domain_records-request2.json b/providers/dns/keyhelp/internal/fixtures/update_domain_records-request2.json deleted file mode 100644 index 3ebb2ee7a..000000000 --- a/providers/dns/keyhelp/internal/fixtures/update_domain_records-request2.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "dkim_record": "default._domainkey IN TXT ( \"v=DKIM1; k=rsa; s=email; \" \"...DKIM KEY...\" )", - "records": { - "soa": { - "ttl": 86400, - "primary_ns": "ns.example.com.", - "rname": "root.example.com.", - "refresh": 14400, - "retry": 1800, - "expire": 604800, - "minimum": 3600 - }, - "other": [ - { - "host": "@", - "ttl": 86400, - "type": "A", - "value": "192.168.178.1" - } - ] - } -} diff --git a/providers/dns/keyhelp/internal/fixtures/update_domain_records.json b/providers/dns/keyhelp/internal/fixtures/update_domain_records.json deleted file mode 100644 index a335b5ba5..000000000 --- a/providers/dns/keyhelp/internal/fixtures/update_domain_records.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "id": 8 -} diff --git a/providers/dns/keyhelp/internal/types.go b/providers/dns/keyhelp/internal/types.go deleted file mode 100644 index 8716fa0c8..000000000 --- a/providers/dns/keyhelp/internal/types.go +++ /dev/null @@ -1,63 +0,0 @@ -package internal - -import ( - "fmt" -) - -type APIError struct { - Code string `json:"code,omitempty"` - Message string `json:"message,omitempty"` -} - -func (a *APIError) Error() string { - return fmt.Sprintf("%s: %s", a.Code, a.Message) -} - -type Domain struct { - ID int `json:"id,omitempty"` - UserID int `json:"id_user,omitempty"` - ParentDomainID int `json:"id_parent_domain,omitempty"` - Status int `json:"status,omitempty"` - Domain string `json:"domain,omitempty"` - DomainUTF8 string `json:"domain_utf8,omitempty"` - IsDisabled bool `json:"is_disabled,omitempty"` - IsCustomDNS bool `json:"is_custom_dns,omitempty"` - IsDNSDisabled bool `json:"is_dns_disabled,omitempty"` - IsSubdomain bool `json:"is_subdomain,omitempty"` - IsSystemDomain bool `json:"is_system_domain,omitempty"` - IsEmailDomain bool `json:"is_email_domain,omitempty"` - IsEmailSendingOnly bool `json:"is_email_sending_only,omitempty"` -} - -type DomainID struct { - ID int `json:"id,omitempty"` -} - -type DomainRecords struct { - IsCustomDNS bool `json:"is_custom_dns,omitempty"` - IsDNSDisabled bool `json:"is_dns_disabled,omitempty"` - DkimRecord string `json:"dkim_record,omitempty"` - Records *Records `json:"records,omitempty"` -} - -type Records struct { - Soa *SOARecord `json:"soa,omitempty"` - Other []Record `json:"other,omitempty"` -} - -type SOARecord struct { - TTL int `json:"ttl,omitempty"` - PrimaryNs string `json:"primary_ns,omitempty"` - RName string `json:"rname,omitempty"` - Refresh int `json:"refresh,omitempty"` - Retry int `json:"retry,omitempty"` - Expire int `json:"expire,omitempty"` - Minimum int `json:"minimum,omitempty"` -} - -type Record struct { - Host string `json:"host"` - TTL int `json:"ttl"` - Type string `json:"type"` - Value string `json:"value"` -} diff --git a/providers/dns/keyhelp/keyhelp.go b/providers/dns/keyhelp/keyhelp.go deleted file mode 100644 index 67ceaaa63..000000000 --- a/providers/dns/keyhelp/keyhelp.go +++ /dev/null @@ -1,225 +0,0 @@ -// Package keyhelp implements a DNS provider for solving the DNS-01 challenge using KeyHelp. -package keyhelp - -import ( - "context" - "errors" - "fmt" - "net/http" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/keyhelp/internal" -) - -// Environment variables names. -const ( - envNamespace = "KEYHELP_" - - EnvBaseURL = envNamespace + "BASE_URL" - EnvAPIKey = envNamespace + "API_KEY" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - BaseURL string - APIKey string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client - - domainIDs map[string]int - domainIDsMu sync.Mutex -} - -// NewDNSProvider returns a DNSProvider instance configured for KeyHelp. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvBaseURL, EnvAPIKey) - if err != nil { - return nil, fmt.Errorf("keyhelp: %w", err) - } - - config := NewDefaultConfig() - config.BaseURL = values[EnvBaseURL] - config.APIKey = values[EnvAPIKey] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for KeyHelp. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("keyhelp: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.BaseURL, config.APIKey) - if err != nil { - return nil, fmt.Errorf("keyhelp: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - domainIDs: make(map[string]int), - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("keyhelp: could not find zone for domain %q: %w", domain, err) - } - - ctx := context.Background() - - domainInfo, err := d.findDomain(ctx, dns01.UnFqdn(authZone)) - if err != nil { - return fmt.Errorf("keyhelp: %w", err) - } - - domainRecords, err := d.client.ListDomainRecords(ctx, domainInfo.ID) - if err != nil { - return fmt.Errorf("keyhelp: list domain records: %w", err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("keyhelp: %w", err) - } - - records := domainRecords.Records.Other - records = append(records, internal.Record{ - Host: subDomain, - TTL: d.config.TTL, - Type: "TXT", - Value: info.Value, - }) - - req := internal.DomainRecords{ - DkimRecord: domainRecords.DkimRecord, - Records: &internal.Records{ - Soa: domainRecords.Records.Soa, - Other: records, - }, - } - - _, err = d.client.UpdateDomainRecords(ctx, domainInfo.ID, req) - if err != nil { - return fmt.Errorf("keyhelp: update domain records (add): %w", err) - } - - d.domainIDsMu.Lock() - d.domainIDs[token] = domainInfo.ID - d.domainIDsMu.Unlock() - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - // get the domain's unique ID from when we created it - d.domainIDsMu.Lock() - domainID, ok := d.domainIDs[token] - d.domainIDsMu.Unlock() - - if !ok { - return fmt.Errorf("keyhelp: unknown record ID for '%s'", info.EffectiveFQDN) - } - - domainRecords, err := d.client.ListDomainRecords(ctx, domainID) - if err != nil { - return fmt.Errorf("keyhelp: list domain records: %w", err) - } - - var records []internal.Record - - for _, record := range domainRecords.Records.Other { - if record.Type == "TXT" && record.Value == info.Value { - continue - } - - records = append(records, record) - } - - req := internal.DomainRecords{ - DkimRecord: domainRecords.DkimRecord, - Records: &internal.Records{ - Soa: domainRecords.Records.Soa, - Other: records, - }, - } - - _, err = d.client.UpdateDomainRecords(ctx, domainID, req) - if err != nil { - return fmt.Errorf("keyhelp: update domain records (delete): %w", err) - } - - // Delete domain ID from map - d.domainIDsMu.Lock() - delete(d.domainIDs, token) - d.domainIDsMu.Unlock() - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -func (d *DNSProvider) findDomain(ctx context.Context, zone string) (internal.Domain, error) { - domains, err := d.client.ListDomains(ctx) - if err != nil { - return internal.Domain{}, fmt.Errorf("list domains: %w", err) - } - - for _, domain := range domains { - if domain.DomainUTF8 == zone || domain.Domain == zone { - return domain, nil - } - } - - return internal.Domain{}, fmt.Errorf("domain not found: %s", zone) -} diff --git a/providers/dns/keyhelp/keyhelp.toml b/providers/dns/keyhelp/keyhelp.toml deleted file mode 100644 index e622794ca..000000000 --- a/providers/dns/keyhelp/keyhelp.toml +++ /dev/null @@ -1,24 +0,0 @@ -Name = "KeyHelp" -Description = '''''' -URL = "https://www.keyweb.de/en/keyhelp/keyhelp/" -Code = "keyhelp" -Since = "v4.26.0" - -Example = ''' -KEYHELP_BASE_URL="https://keyhelp.example.com" \ -KEYHELP_API_KEY="xxx" \ -lego --dns keyhelp -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - KEYHELP_BASE_URL= "Server URL" - KEYHELP_API_KEY = "API key" - [Configuration.Additional] - KEYHELP_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - KEYHELP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - KEYHELP_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - KEYHELP_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://app.swaggerhub.com/apis-docs/keyhelp/api/" diff --git a/providers/dns/keyhelp/keyhelp_test.go b/providers/dns/keyhelp/keyhelp_test.go deleted file mode 100644 index 8d8ac821d..000000000 --- a/providers/dns/keyhelp/keyhelp_test.go +++ /dev/null @@ -1,198 +0,0 @@ -package keyhelp - -import ( - "net/http/httptest" - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/go-acme/lego/v4/providers/dns/keyhelp/internal" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvBaseURL, EnvAPIKey). - WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvBaseURL: "https://keyhelp.example.com", - EnvAPIKey: "secret", - }, - }, - { - desc: "missing base URL", - envVars: map[string]string{ - EnvAPIKey: "secret", - }, - expected: "keyhelp: some credentials information are missing: KEYHELP_BASE_URL", - }, - { - desc: "missing API key", - envVars: map[string]string{ - EnvBaseURL: "https://keyhelp.example.com", - }, - expected: "keyhelp: some credentials information are missing: KEYHELP_API_KEY", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "keyhelp: some credentials information are missing: KEYHELP_BASE_URL,KEYHELP_API_KEY", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - baseURL string - apiKey string - expected string - }{ - { - desc: "success", - baseURL: "https://keyhelp.example.com", - apiKey: "secret", - }, - { - desc: "missing base URL", - apiKey: "secret", - expected: "keyhelp: missing base URL", - }, - { - desc: "missing API key", - baseURL: "https://keyhelp.example.com", - expected: "keyhelp: credentials missing", - }, - { - desc: "missing credentials", - expected: "keyhelp: missing base URL", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.BaseURL = test.baseURL - config.APIKey = test.apiKey - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.HTTPClient = server.Client() - config.APIKey = "secret" - config.BaseURL = server.URL - - return NewDNSProviderConfig(config) - }, - servermock.CheckHeader(). - With(internal.APIKeyHeader, "secret"), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("GET /api/v2/domains", - servermock.ResponseFromInternal("get_domains.json"), - servermock.CheckQueryParameter(). - With("sort", "domain_utf8"). - Strict()). - Route("GET /api/v2/dns/8", - servermock.ResponseFromInternal("get_domain_records.json")). - Route("PUT /api/v2/dns/8", - servermock.ResponseFromInternal("update_domain_records.json"), - servermock.CheckRequestJSONBodyFromInternal("update_domain_records-request.json")). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) - - assert.Equal(t, 8, provider.domainIDs["abc"]) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("GET /api/v2/dns/8", - servermock.ResponseFromInternal("get_domain_records2.json")). - Route("PUT /api/v2/dns/8", - servermock.ResponseFromInternal("update_domain_records.json"), - servermock.CheckRequestJSONBodyFromInternal("update_domain_records-request2.json")). - Build(t) - - provider.domainIDs["abc"] = 8 - - err := provider.CleanUp("example.com", "abc", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/leaseweb/internal/client.go b/providers/dns/leaseweb/internal/client.go deleted file mode 100644 index 01619d49b..000000000 --- a/providers/dns/leaseweb/internal/client.go +++ /dev/null @@ -1,216 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" -) - -const defaultBaseURL = "https://api.leaseweb.com/hosting/v2" - -const AuthHeader = "X-LSW-Auth" - -// Client the Leaseweb API client. -type Client struct { - apiKey string - - BaseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(apiKey string) (*Client, error) { - if apiKey == "" { - return nil, errors.New("credentials missing") - } - - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - apiKey: apiKey, - BaseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -// CreateRRSet creates a resource record set. -// https://developer.leaseweb.com/docs/#tag/DNS/operation/createResourceRecordSet -func (c *Client) CreateRRSet(ctx context.Context, domainName string, rrset RRSet) (*RRSet, error) { - endpoint := c.BaseURL.JoinPath("domains", domainName, "resourceRecordSets") - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, rrset) - if err != nil { - return nil, err - } - - result := &RRSet{} - - err = c.do(req, result) - if err != nil { - return nil, err - } - - return result, nil -} - -// GetRRSet gets a resource record set. -// https://developer.leaseweb.com/docs/#tag/DNS/operation/getResourceRecordSet -func (c *Client) GetRRSet(ctx context.Context, domainName, name, rType string) (*RRSet, error) { - endpoint := c.BaseURL.JoinPath("domains", domainName, "resourceRecordSets", name, rType) - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - result := &RRSet{} - - err = c.do(req, result) - if err != nil { - return nil, err - } - - return result, nil -} - -// UpdateRRSet updates a resource record set. -// https://developer.leaseweb.com/docs/#tag/DNS/operation/updateResourceRecordSet -func (c *Client) UpdateRRSet(ctx context.Context, domainName string, rrset RRSet) (*RRSet, error) { - endpoint := c.BaseURL.JoinPath("domains", domainName, "resourceRecordSets", rrset.Name, rrset.Type) - - // Reset values that are not allowed to be updated. - rrset.Name = "" - rrset.Type = "" - rrset.Editable = false - - req, err := newJSONRequest(ctx, http.MethodPut, endpoint, rrset) - if err != nil { - return nil, err - } - - result := &RRSet{} - - err = c.do(req, result) - if err != nil { - return nil, err - } - - return result, nil -} - -// DeleteRRSet deletes a resource record set. -// https://developer.leaseweb.com/docs/#tag/DNS/operation/deleteResourceRecordSet -func (c *Client) DeleteRRSet(ctx context.Context, domainName, name, rType string) error { - endpoint := c.BaseURL.JoinPath("domains", domainName, "resourceRecordSets", name, rType) - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return err - } - - return c.do(req, nil) -} - -func (c *Client) do(req *http.Request, result any) error { - useragent.SetHeader(req.Header) - - req.Header.Add(AuthHeader, c.apiKey) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIError - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - if resp.StatusCode == http.StatusNotFound { - return &NotFoundError{APIError{ - CorrelationID: resp.Header.Get("Correlation-Id"), - ErrorCode: strconv.Itoa(http.StatusNotFound), - ErrorMessage: string(raw), - }} - } - - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - if errAPI.ErrorCode == strconv.Itoa(http.StatusNotFound) { - return &NotFoundError{APIError: errAPI} - } - - return &errAPI -} - -// TTLRounder rounds the given TTL in seconds to the next accepted value. -// Accepted TTL values are: 60, 300, 1800, 3600, 14400, 28800, 43200, 86400. -func TTLRounder(ttl int) int { - for _, validTTL := range []int{60, 300, 1800, 3600, 14400, 28800, 43200, 86400} { - if ttl <= validTTL { - return validTTL - } - } - - return 3600 -} diff --git a/providers/dns/leaseweb/internal/client_test.go b/providers/dns/leaseweb/internal/client_test.go deleted file mode 100644 index 5762aad4b..000000000 --- a/providers/dns/leaseweb/internal/client_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("secret") - if err != nil { - return nil, err - } - - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(). - With(AuthHeader, "secret"), - ) -} - -func TestClient_CreateRRSet(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/example.com/resourceRecordSets", - servermock.ResponseFromFixture("createResourceRecordSet.json"), - servermock.CheckRequestJSONBodyFromFixture("createResourceRecordSet-request.json"), - ). - Build(t) - - rrset := RRSet{ - Content: []string{"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"}, - Name: "_acme-challenge.example.com.", - TTL: 300, - Type: "TXT", - } - - result, err := client.CreateRRSet(t.Context(), "example.com", rrset) - require.NoError(t, err) - - expected := &RRSet{ - Content: []string{"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"}, - Name: "_acme-challenge.example.com.", - Editable: true, - TTL: 300, - Type: "TXT", - } - - assert.Equal(t, expected, result) -} - -func TestClient_GetRRSet(t *testing.T) { - client := mockBuilder(). - Route("GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", - servermock.ResponseFromFixture("getResourceRecordSet.json"), - ). - Build(t) - - result, err := client.GetRRSet(t.Context(), "example.com", "_acme-challenge.example.com.", "TXT") - require.NoError(t, err) - - expected := &RRSet{ - Content: []string{"foo", "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo"}, - Name: "_acme-challenge.example.com.", - Editable: true, - TTL: 3600, - Type: "TXT", - } - - assert.Equal(t, expected, result) -} - -func TestClient_GetRRSet_error_404(t *testing.T) { - client := mockBuilder(). - Route("GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", - servermock.ResponseFromFixture("error_404.json"). - WithStatusCode(http.StatusNotFound), - ). - Build(t) - - _, err := client.GetRRSet(t.Context(), "example.com", "_acme-challenge.example.com.", "TXT") - require.EqualError(t, err, "404: Resource not found (289346a1-3eaf-4da4-b707-62ef12eb08be)") - - target := &NotFoundError{} - require.ErrorAs(t, err, &target) -} - -func TestClient_UpdateRRSet(t *testing.T) { - client := mockBuilder(). - Route("PUT /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", - servermock.ResponseFromFixture("updateResourceRecordSet.json"), - servermock.CheckRequestJSONBodyFromFixture("updateResourceRecordSet-request.json"), - ). - Build(t) - - rrset := RRSet{ - Content: []string{"foo", "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"}, - Name: "_acme-challenge.example.com.", - TTL: 3600, - Type: "TXT", - } - - result, err := client.UpdateRRSet(t.Context(), "example.com", rrset) - require.NoError(t, err) - - expected := &RRSet{ - Content: []string{"foo", "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"}, - Name: "_acme-challenge.example.com.", - Editable: true, - TTL: 3600, - Type: "TXT", - } - - assert.Equal(t, expected, result) -} - -func TestClient_DeleteRRSet(t *testing.T) { - client := mockBuilder(). - Route("DELETE /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", - servermock.Noop(). - WithStatusCode(http.StatusNoContent), - ). - Build(t) - - err := client.DeleteRRSet(t.Context(), "example.com", "_acme-challenge.example.com.", "TXT") - require.NoError(t, err) -} - -func TestClient_DeleteRRSet_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", - servermock.ResponseFromFixture("error_401.json"). - WithStatusCode(http.StatusUnauthorized), - ). - Build(t) - - err := client.DeleteRRSet(t.Context(), "example.com", "_acme-challenge.example.com.", "TXT") - require.EqualError(t, err, "401: You are not authorized to view this resource. (289346a1-3eaf-4da4-b707-62ef12eb08be)") -} diff --git a/providers/dns/leaseweb/internal/fixtures/createResourceRecordSet-request.json b/providers/dns/leaseweb/internal/fixtures/createResourceRecordSet-request.json deleted file mode 100644 index af53fcf04..000000000 --- a/providers/dns/leaseweb/internal/fixtures/createResourceRecordSet-request.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "content": [ - "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" - ], - "name": "_acme-challenge.example.com.", - "ttl": 300, - "type": "TXT" -} diff --git a/providers/dns/leaseweb/internal/fixtures/createResourceRecordSet.json b/providers/dns/leaseweb/internal/fixtures/createResourceRecordSet.json deleted file mode 100644 index 8ca040d63..000000000 --- a/providers/dns/leaseweb/internal/fixtures/createResourceRecordSet.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "_links": { - "self": { - "href": "/domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT" - }, - "collection": { - "href": "/domains/example.com/resourceRecordSets" - } - }, - "content": [ - "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" - ], - "editable": true, - "name": "_acme-challenge.example.com.", - "ttl": 300, - "type": "TXT" -} diff --git a/providers/dns/leaseweb/internal/fixtures/error_400.json b/providers/dns/leaseweb/internal/fixtures/error_400.json deleted file mode 100644 index 1a980b6bb..000000000 --- a/providers/dns/leaseweb/internal/fixtures/error_400.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "correlationId": "289346a1-3eaf-4da4-b707-62ef12eb08be", - "errorCode": "400", - "errorDetails": {}, - "errorMessage": "The API could not interpret your request correctly." -} diff --git a/providers/dns/leaseweb/internal/fixtures/error_401.json b/providers/dns/leaseweb/internal/fixtures/error_401.json deleted file mode 100644 index 47d8a311d..000000000 --- a/providers/dns/leaseweb/internal/fixtures/error_401.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "correlationId": "289346a1-3eaf-4da4-b707-62ef12eb08be", - "errorCode": "401", - "errorMessage": "You are not authorized to view this resource." -} diff --git a/providers/dns/leaseweb/internal/fixtures/error_404.json b/providers/dns/leaseweb/internal/fixtures/error_404.json deleted file mode 100644 index 1deaf5606..000000000 --- a/providers/dns/leaseweb/internal/fixtures/error_404.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "correlationId": "289346a1-3eaf-4da4-b707-62ef12eb08be", - "errorCode": "404", - "errorMessage": "Resource not found" -} diff --git a/providers/dns/leaseweb/internal/fixtures/getResourceRecordSet.json b/providers/dns/leaseweb/internal/fixtures/getResourceRecordSet.json deleted file mode 100644 index fd48f60c6..000000000 --- a/providers/dns/leaseweb/internal/fixtures/getResourceRecordSet.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "_links": { - "self": { - "href": "/domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT" - }, - "collection": { - "href": "/domains/example.com/resourceRecordSets" - } - }, - "content": [ - "foo", - "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo" - ], - "editable": true, - "name": "_acme-challenge.example.com.", - "ttl": 3600, - "type": "TXT" -} diff --git a/providers/dns/leaseweb/internal/fixtures/getResourceRecordSet2.json b/providers/dns/leaseweb/internal/fixtures/getResourceRecordSet2.json deleted file mode 100644 index abf3fb4c3..000000000 --- a/providers/dns/leaseweb/internal/fixtures/getResourceRecordSet2.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "_links": { - "self": { - "href": "/domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT" - }, - "collection": { - "href": "/domains/example.com/resourceRecordSets" - } - }, - "content": [ - "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo" - ], - "editable": true, - "name": "_acme-challenge.example.com.", - "ttl": 3600, - "type": "TXT" -} diff --git a/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet-request.json b/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet-request.json deleted file mode 100644 index e781958c8..000000000 --- a/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet-request.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "content": [ - "foo", - "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo", - "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" - ], - "ttl": 3600 -} diff --git a/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet-request2.json b/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet-request2.json deleted file mode 100644 index 0acc314de..000000000 --- a/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet-request2.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "content": [ - "foo" - ], - "ttl": 3600 -} diff --git a/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet.json b/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet.json deleted file mode 100644 index 2b877982c..000000000 --- a/providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "_links": { - "self": { - "href": "/domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT" - }, - "collection": { - "href": "/domains/example.com/resourceRecordSets" - } - }, - "content": [ - "foo", - "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo", - "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" - ], - "editable": true, - "name": "_acme-challenge.example.com.", - "ttl": 3600, - "type": "TXT" -} diff --git a/providers/dns/leaseweb/internal/types.go b/providers/dns/leaseweb/internal/types.go deleted file mode 100644 index 7a4547584..000000000 --- a/providers/dns/leaseweb/internal/types.go +++ /dev/null @@ -1,35 +0,0 @@ -package internal - -import ( - "encoding/json" - "fmt" -) - -type NotFoundError struct { - APIError -} - -type APIError struct { - CorrelationID string `json:"correlationId,omitempty"` - ErrorCode string `json:"errorCode,omitempty"` - ErrorMessage string `json:"errorMessage,omitempty"` - ErrorDetails json.RawMessage `json:"errorDetails,omitempty"` -} - -func (a *APIError) Error() string { - msg := fmt.Sprintf("%s: %s (%s)", a.ErrorCode, a.ErrorMessage, a.CorrelationID) - - if len(a.ErrorDetails) > 0 { - msg += fmt.Sprintf(": %s", string(a.ErrorDetails)) - } - - return msg -} - -type RRSet struct { - Content []string `json:"content,omitempty"` - Name string `json:"name,omitempty"` - Editable bool `json:"editable,omitempty"` - TTL int `json:"ttl,omitempty"` - Type string `json:"type,omitempty"` -} diff --git a/providers/dns/leaseweb/leaseweb.go b/providers/dns/leaseweb/leaseweb.go deleted file mode 100644 index fafaf1c4d..000000000 --- a/providers/dns/leaseweb/leaseweb.go +++ /dev/null @@ -1,187 +0,0 @@ -// Package leaseweb implements a DNS provider for solving the DNS-01 challenge using Leaseweb. -package leaseweb - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/leaseweb/internal" -) - -// Environment variables names. -const ( - envNamespace = "LEASEWEB_" - - EnvAPIKey = envNamespace + "API_KEY" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for Leaseweb. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIKey) - if err != nil { - return nil, fmt.Errorf("leaseweb: %w", err) - } - - config := NewDefaultConfig() - config.APIKey = values[EnvAPIKey] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Leaseweb. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("leaseweb: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.APIKey) - if err != nil { - return nil, fmt.Errorf("leaseweb: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("leaseweb: could not find zone for domain %q: %w", domain, err) - } - - existingRRSet, err := d.client.GetRRSet(ctx, dns01.UnFqdn(authZone), info.EffectiveFQDN, "TXT") - if err != nil { - notfoundErr := &internal.NotFoundError{} - if !errors.As(err, ¬foundErr) { - return fmt.Errorf("leaseweb: get RRSet: %w", err) - } - - // Create the RRSet. - - rrset := internal.RRSet{ - Content: []string{info.Value}, - Name: info.EffectiveFQDN, - TTL: internal.TTLRounder(d.config.TTL), - Type: "TXT", - } - - _, err = d.client.CreateRRSet(ctx, dns01.UnFqdn(authZone), rrset) - if err != nil { - return fmt.Errorf("leaseweb: create RRSet: %w", err) - } - - return nil - } - - // Update the RRSet. - - existingRRSet.Content = append(existingRRSet.Content, info.Value) - - _, err = d.client.UpdateRRSet(ctx, dns01.UnFqdn(authZone), *existingRRSet) - if err != nil { - return fmt.Errorf("leaseweb: update RRSet: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("leaseweb: could not find zone for domain %q: %w", domain, err) - } - - existingRRSet, err := d.client.GetRRSet(ctx, dns01.UnFqdn(authZone), info.EffectiveFQDN, "TXT") - if err != nil { - return fmt.Errorf("leaseweb: get RRSet: %w", err) - } - - var content []string - - for _, s := range existingRRSet.Content { - if s != info.Value { - content = append(content, s) - } - } - - if len(content) == 0 { - err = d.client.DeleteRRSet(ctx, dns01.UnFqdn(authZone), info.EffectiveFQDN, "TXT") - if err != nil { - return fmt.Errorf("leaseweb: delete RRSet: %w", err) - } - - return nil - } - - existingRRSet.Content = content - - _, err = d.client.UpdateRRSet(ctx, dns01.UnFqdn(authZone), *existingRRSet) - if err != nil { - return fmt.Errorf("leaseweb: update RRSet: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/leaseweb/leaseweb.toml b/providers/dns/leaseweb/leaseweb.toml deleted file mode 100644 index 2c3503291..000000000 --- a/providers/dns/leaseweb/leaseweb.toml +++ /dev/null @@ -1,22 +0,0 @@ -Name = "Leaseweb" -Description = '''''' -URL = "https://www.leaseweb.com/en/" -Code = "leaseweb" -Since = "v4.32.0" - -Example = ''' -LEASEWEB_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns leaseweb -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - LEASEWEB_API_KEY = "API key" - [Configuration.Additional] - LEASEWEB_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - LEASEWEB_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - LEASEWEB_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - LEASEWEB_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://developer.leaseweb.com/docs/#tag/DNS" diff --git a/providers/dns/leaseweb/leaseweb_test.go b/providers/dns/leaseweb/leaseweb_test.go deleted file mode 100644 index 0450cd2c2..000000000 --- a/providers/dns/leaseweb/leaseweb_test.go +++ /dev/null @@ -1,204 +0,0 @@ -package leaseweb - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/go-acme/lego/v4/providers/dns/leaseweb/internal" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAPIKey: "secret", - }, - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "leaseweb: some credentials information are missing: LEASEWEB_API_KEY", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiKey string - expected string - }{ - { - desc: "success", - apiKey: "secret", - }, - { - desc: "missing credentials", - expected: "leaseweb: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIKey = test.apiKey - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIKey = "secret" - config.HTTPClient = server.Client() - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - p.client.BaseURL, _ = url.Parse(server.URL) - - return p, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(). - With(internal.AuthHeader, "secret"), - ) -} - -func TestDNSProvider_Present_create(t *testing.T) { - provider := mockBuilder(). - Route("GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", - servermock.ResponseFromInternal("error_404.json"). - WithStatusCode(http.StatusNotFound), - ). - Route("POST /domains/example.com/resourceRecordSets", - servermock.ResponseFromInternal("createResourceRecordSet.json"), - servermock.CheckRequestJSONBodyFromInternal("createResourceRecordSet-request.json"), - ). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_Present_update(t *testing.T) { - provider := mockBuilder(). - Route("GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", - servermock.ResponseFromInternal("getResourceRecordSet.json"), - ). - Route("PUT /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", - servermock.ResponseFromInternal("updateResourceRecordSet.json"), - servermock.CheckRequestJSONBodyFromInternal("updateResourceRecordSet-request.json"), - ). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp_delete(t *testing.T) { - provider := mockBuilder(). - Route("GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", - servermock.ResponseFromInternal("getResourceRecordSet2.json"), - ). - Route("DELETE /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", - servermock.Noop(). - WithStatusCode(http.StatusNoContent), - ). - Build(t) - - err := provider.CleanUp("example.com", "abc", "1234d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp_update(t *testing.T) { - provider := mockBuilder(). - Route("GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", - servermock.ResponseFromInternal("getResourceRecordSet.json"), - ). - Route("PUT /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT", - servermock.ResponseFromInternal("updateResourceRecordSet.json"), - servermock.CheckRequestJSONBodyFromInternal("updateResourceRecordSet-request2.json"), - ). - Build(t) - - err := provider.CleanUp("example.com", "abc", "1234d==") - require.NoError(t, err) -} diff --git a/providers/dns/liara/internal/client.go b/providers/dns/liara/internal/client.go index 95c39695b..89794f04d 100644 --- a/providers/dns/liara/internal/client.go +++ b/providers/dns/liara/internal/client.go @@ -20,31 +20,25 @@ const defaultBaseURL = "https://dns-service.iran.liara.ir" type Client struct { baseURL *url.URL httpClient *http.Client - - teamID string } // NewClient creates a new Client. -func NewClient(hc *http.Client, teamID string) *Client { +func NewClient(hc *http.Client) *Client { baseURL, _ := url.Parse(defaultBaseURL) if hc == nil { hc = &http.Client{Timeout: 10 * time.Second} } - return &Client{ - httpClient: hc, - baseURL: baseURL, - teamID: teamID, - } + return &Client{httpClient: hc, baseURL: baseURL} } // GetRecords gets the records of a domain. -// https://openapi.liara.ir/?urls.primaryName=DNS -func (c *Client) GetRecords(ctx context.Context, domainName string) ([]Record, error) { +// https://dns-service.iran.liara.ir/swagger +func (c Client) GetRecords(ctx context.Context, domainName string) ([]Record, error) { endpoint := c.baseURL.JoinPath("api", "v1", "zones", domainName, "dns-records") - req, err := c.newJSONRequest(ctx, http.MethodGet, endpoint, nil) + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, fmt.Errorf("create request: %w", err) } @@ -66,7 +60,6 @@ func (c *Client) GetRecords(ctx context.Context, domainName string) ([]Record, e } var response Response[[]Record] - err = json.Unmarshal(raw, &response) if err != nil { return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) @@ -76,10 +69,10 @@ func (c *Client) GetRecords(ctx context.Context, domainName string) ([]Record, e } // CreateRecord creates a record. -func (c *Client) CreateRecord(ctx context.Context, domainName string, record Record) (*Record, error) { +func (c Client) CreateRecord(ctx context.Context, domainName string, record Record) (*Record, error) { endpoint := c.baseURL.JoinPath("api", "v1", "zones", domainName, "dns-records") - req, err := c.newJSONRequest(ctx, http.MethodPost, endpoint, record) + req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) if err != nil { return nil, fmt.Errorf("create request: %w", err) } @@ -101,7 +94,6 @@ func (c *Client) CreateRecord(ctx context.Context, domainName string, record Rec } var response Response[*Record] - err = json.Unmarshal(raw, &response) if err != nil { return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) @@ -111,10 +103,10 @@ func (c *Client) CreateRecord(ctx context.Context, domainName string, record Rec } // GetRecord gets a specific record. -func (c *Client) GetRecord(ctx context.Context, domainName, recordID string) (*Record, error) { +func (c Client) GetRecord(ctx context.Context, domainName, recordID string) (*Record, error) { endpoint := c.baseURL.JoinPath("api", "v1", "zones", domainName, "dns-records", recordID) - req, err := c.newJSONRequest(ctx, http.MethodGet, endpoint, nil) + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, fmt.Errorf("create request: %w", err) } @@ -136,7 +128,6 @@ func (c *Client) GetRecord(ctx context.Context, domainName, recordID string) (*R } var response Response[*Record] - err = json.Unmarshal(raw, &response) if err != nil { return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) @@ -146,10 +137,10 @@ func (c *Client) GetRecord(ctx context.Context, domainName, recordID string) (*R } // DeleteRecord deletes a record. -func (c *Client) DeleteRecord(ctx context.Context, domainName, recordID string) error { +func (c Client) DeleteRecord(ctx context.Context, domainName, recordID string) error { endpoint := c.baseURL.JoinPath("api", "v1", "zones", domainName, "dns-records", recordID) - req, err := c.newJSONRequest(ctx, http.MethodDelete, endpoint, nil) + req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) if err != nil { return fmt.Errorf("create request: %w", err) } @@ -168,14 +159,7 @@ func (c *Client) DeleteRecord(ctx context.Context, domainName, recordID string) return nil } -func (c *Client) newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - if c.teamID != "" { - query := endpoint.Query() - query.Set("teamID", c.teamID) - - endpoint.RawQuery = query.Encode() - } - +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { buf := new(bytes.Buffer) if payload != nil { @@ -203,7 +187,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) var errAPI APIError - err := json.Unmarshal(raw, &errAPI) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/liara/internal/client_test.go b/providers/dns/liara/internal/client_test.go index b6d007046..ed6672ab6 100644 --- a/providers/dns/liara/internal/client_test.go +++ b/providers/dns/liara/internal/client_test.go @@ -1,36 +1,28 @@ package internal import ( + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const apiKey = "key" -func mockBuilder(teamID string) *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient(OAuthStaticAccessToken(server.Client(), apiKey), teamID) - client.baseURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("Bearer "+apiKey)) -} - func TestClient_GetRecords(t *testing.T) { - client := mockBuilder(""). - Route("GET /api/v1/zones/example.com/dns-records", servermock.ResponseFromFixture("RecordsResponse.json")). - Build(t) + client, mux := setupTest(t) - records, err := client.GetRecords(t.Context(), "example.com") + mux.HandleFunc("/api/v1/zones/example.com/dns-records", testHandler("./RecordsResponse.json", http.MethodGet, http.StatusOK)) + + records, err := client.GetRecords(context.Background(), "example.com") require.NoError(t, err) expected := []Record{ @@ -50,11 +42,11 @@ func TestClient_GetRecords(t *testing.T) { } func TestClient_GetRecord(t *testing.T) { - client := mockBuilder(""). - Route("GET /api/v1/zones/example.com/dns-records/123", servermock.ResponseFromFixture("RecordResponse.json")). - Build(t) + client, mux := setupTest(t) - record, err := client.GetRecord(t.Context(), "example.com", "123") + mux.HandleFunc("/api/v1/zones/example.com/dns-records/123", testHandler("./RecordResponse.json", http.MethodGet, http.StatusOK)) + + record, err := client.GetRecord(context.Background(), "example.com", "123") require.NoError(t, err) expected := &Record{ @@ -72,12 +64,9 @@ func TestClient_GetRecord(t *testing.T) { } func TestClient_CreateRecord(t *testing.T) { - client := mockBuilder(""). - Route("POST /api/v1/zones/example.com/dns-records", - servermock.ResponseFromFixture("RecordResponse.json"). - WithStatusCode(http.StatusCreated), - servermock.CheckRequestJSONBody(`{"name":"string","type":"string","ttl":3600,"contents":[{"text":"string"}]}`)). - Build(t) + client, mux := setupTest(t) + + mux.HandleFunc("/api/v1/zones/example.com/dns-records", testHandler("./RecordResponse.json", http.MethodPost, http.StatusCreated)) data := Record{ Type: "string", @@ -90,46 +79,7 @@ func TestClient_CreateRecord(t *testing.T) { TTL: 3600, } - record, err := client.CreateRecord(t.Context(), "example.com", data) - require.NoError(t, err) - - expected := &Record{ - ID: "string", - Type: "string", - Name: "string", - Contents: []Content{ - { - Text: "string", - }, - }, - TTL: 3600, - } - - assert.Equal(t, expected, record) -} - -func TestClient_CreateRecord_withTeamID(t *testing.T) { - client := mockBuilder("123"). - Route("POST /api/v1/zones/example.com/dns-records", - servermock.ResponseFromFixture("RecordResponse.json"). - WithStatusCode(http.StatusCreated), - servermock.CheckRequestJSONBody(`{"name":"string","type":"string","ttl":3600,"contents":[{"text":"string"}]}`), - servermock.CheckQueryParameter().Strict().With("teamID", "123"), - ). - Build(t) - - data := Record{ - Type: "string", - Name: "string", - Contents: []Content{ - { - Text: "string", - }, - }, - TTL: 3600, - } - - record, err := client.CreateRecord(t.Context(), "example.com", data) + record, err := client.CreateRecord(context.Background(), "example.com", data) require.NoError(t, err) expected := &Record{ @@ -148,34 +98,76 @@ func TestClient_CreateRecord_withTeamID(t *testing.T) { } func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(""). - Route("DELETE /api/v1/zones/example.com/dns-records/123", - servermock.Noop(). - WithStatusCode(http.StatusNoContent)). - Build(t) + client, mux := setupTest(t) - err := client.DeleteRecord(t.Context(), "example.com", "123") + mux.HandleFunc("/api/v1/zones/example.com/dns-records/123", func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusNoContent) + }) + + err := client.DeleteRecord(context.Background(), "example.com", "123") require.NoError(t, err) } func TestClient_DeleteRecord_NotFound_Response(t *testing.T) { - client := mockBuilder(""). - Route("DELETE /api/v1/zones/example.com/dns-records/123", - servermock.Noop(). - WithStatusCode(http.StatusNotFound)). - Build(t) + client, mux := setupTest(t) - err := client.DeleteRecord(t.Context(), "example.com", "123") + mux.HandleFunc("/api/v1/zones/example.com/dns-records/123", func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusNotFound) + }) + + err := client.DeleteRecord(context.Background(), "example.com", "123") require.NoError(t, err) } func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(""). - Route("DELETE /api/v1/zones/example.com/dns-records/123", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client, mux := setupTest(t) - err := client.DeleteRecord(t.Context(), "example.com", "123") - require.EqualError(t, err, "[status code: 401] Unauthorized: Invalid token missing header") + mux.HandleFunc("/api/v1/zones/example.com/dns-records/123", testHandler("./error.json", http.MethodDelete, http.StatusUnauthorized)) + + err := client.DeleteRecord(context.Background(), "example.com", "123") + require.Error(t, err) +} + +func testHandler(filename string, method string, statusCode int) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + auth := req.Header.Get("Authorization") + if auth != "Bearer "+apiKey { + http.Error(rw, "invalid Authorization header", http.StatusUnauthorized) + return + } + + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = file.Close() }() + + rw.WriteHeader(statusCode) + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + } +} + +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient(OAuthStaticAccessToken(server.Client(), apiKey)) + client.baseURL, _ = url.Parse(server.URL) + + return client, mux } diff --git a/providers/dns/liara/liara.go b/providers/dns/liara/liara.go index c7e403eed..cb4ab7c8d 100644 --- a/providers/dns/liara/liara.go +++ b/providers/dns/liara/liara.go @@ -9,40 +9,33 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/liara/internal" "github.com/hashicorp/go-retryablehttp" ) -// Environment variables names. -const ( - envNamespace = "LIARA_" - - EnvAPIKey = envNamespace + "API_KEY" - EnvTeamID = envNamespace + "TEAM_ID" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - const ( minTTL = 120 maxTTL = 432000 ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) +// Environment variables names. +const ( + envNamespace = "LIARA_" + + EnvAPIKey = envNamespace + "API_KEY" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) // Config is used to configure the creation of the DNSProvider. type Config struct { - APIKey string - TeamID string - + APIKey string TTL int PropagationTimeout time.Duration PollingInterval time.Duration @@ -80,7 +73,6 @@ func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] - config.TeamID = env.GetOrFile(EnvTeamID) return NewDNSProviderConfig(config) } @@ -104,20 +96,13 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { } retryClient := retryablehttp.NewClient() - retryClient.RetryMax = 5 if config.HTTPClient != nil { retryClient.HTTPClient = config.HTTPClient } - retryClient.Logger = log.Logger - client := internal.NewClient( - clientdebug.Wrap( - internal.OAuthStaticAccessToken(retryClient.StandardClient(), config.APIKey), - ), - config.TeamID, - ) + client := internal.NewClient(internal.OAuthStaticAccessToken(retryClient.StandardClient(), config.APIKey)) return &DNSProvider{ config: config, @@ -152,7 +137,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { Contents: []internal.Content{{Text: info.Value}}, TTL: d.config.TTL, } - newRecord, err := d.client.CreateRecord(context.Background(), dns01.UnFqdn(authZone), record) if err != nil { return fmt.Errorf("liara: failed to create TXT record, fqdn=%s: %w", info.EffectiveFQDN, err) @@ -178,7 +162,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() - if !ok { return fmt.Errorf("liara: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) } diff --git a/providers/dns/liara/liara.toml b/providers/dns/liara/liara.toml index f471de04e..323229c5c 100644 --- a/providers/dns/liara/liara.toml +++ b/providers/dns/liara/liara.toml @@ -6,18 +6,17 @@ Since = "v4.10.0" Example = ''' LIARA_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns liara -d '*.example.com' -d example.com run +lego --email myemail@example.com --dns liara --domains my.example.org run ''' [Configuration] [Configuration.Credentials] LIARA_API_KEY = "The API key" [Configuration.Additional] - LIARA_TEAM_ID = "The team ID to access services in a team" - LIARA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - LIARA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - LIARA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)" - LIARA_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + LIARA_POLLING_INTERVAL = "Time between DNS propagation check" + LIARA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + LIARA_TTL = "The TTL of the TXT record used for the DNS challenge" + LIARA_HTTP_TIMEOUT = "API request timeout" [Links] - API = "https://openapi.liara.ir/?urls.primaryName=DNS" + API = "https://dns-service.iran.liara.ir/swagger" diff --git a/providers/dns/liara/liara_test.go b/providers/dns/liara/liara_test.go index b1f3f77c9..4256be55e 100644 --- a/providers/dns/liara/liara_test.go +++ b/providers/dns/liara/liara_test.go @@ -38,7 +38,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -114,7 +113,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -128,7 +126,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/lightsail/lightsail.go b/providers/dns/lightsail/lightsail.go index 95b07c503..125b1aa61 100644 --- a/providers/dns/lightsail/lightsail.go +++ b/providers/dns/lightsail/lightsail.go @@ -14,11 +14,14 @@ import ( awsconfig "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/lightsail" awstypes "github.com/aws/aws-sdk-go-v2/service/lightsail/types" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) +const ( + maxRetries = 5 +) + // Environment variables names. const ( envNamespace = "LIGHTSAIL_" @@ -30,10 +33,6 @@ const ( EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) -const maxRetries = 5 - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { DNSZone string @@ -96,10 +95,12 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // causing a high number of consecutive throttling errors. // For reference: Route 53 enforces an account-wide(!) 5req/s query limit. options.Backoff = retry.BackoffDelayerFunc(func(attempt int, err error) (time.Duration, error) { - retryCount := min(attempt, 7) + retryCount := attempt + if retryCount > 7 { + retryCount = 7 + } delay := (1 << uint(retryCount)) * (rand.Intn(50) + 200) - return time.Duration(delay) * time.Millisecond, nil }) }) diff --git a/providers/dns/lightsail/lightsail.toml b/providers/dns/lightsail/lightsail.toml index 47b212f62..4ade894d1 100644 --- a/providers/dns/lightsail/lightsail.toml +++ b/providers/dns/lightsail/lightsail.toml @@ -52,8 +52,8 @@ Alternatively, you can also set the `Resource` to `*` (wildcard), which allow to DNS_ZONE = "Domain name of the DNS zone" [Configuration.Additional] AWS_SHARED_CREDENTIALS_FILE = "Managed by the AWS client. Shared credentials file." - LIGHTSAIL_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - LIGHTSAIL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" + LIGHTSAIL_POLLING_INTERVAL = "Time between DNS propagation check" + LIGHTSAIL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" [Links] GoClient = "https://github.com/aws/aws-sdk-go-v2" diff --git a/providers/dns/lightsail/lightsail_integration_test.go b/providers/dns/lightsail/lightsail_integration_test.go index dc86bf079..20e45ee26 100644 --- a/providers/dns/lightsail/lightsail_integration_test.go +++ b/providers/dns/lightsail/lightsail_integration_test.go @@ -1,12 +1,12 @@ package lightsail import ( + "context" "testing" "github.com/aws/aws-sdk-go-v2/aws" awsconfig "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/lightsail" - "github.com/go-acme/lego/v4/providers/dns/internal/ptr" "github.com/stretchr/testify/require" ) @@ -28,13 +28,12 @@ func TestLiveTTL(t *testing.T) { // we need a separate Lightsail client here as the one in the DNS provider is unexported. fqdn := "_acme-challenge." + domain - ctx := t.Context() + ctx := context.Background() cfg, err := awsconfig.LoadDefaultConfig(ctx) require.NoError(t, err) svc := lightsail.NewFromConfig(cfg) - require.NoError(t, err) defer func() { @@ -53,10 +52,19 @@ func TestLiveTTL(t *testing.T) { entries := resp.Domain.DomainEntries for _, entry := range entries { - if ptr.Deref(entry.Type) == "TXT" && ptr.Deref(entry.Name) == fqdn { + if deref(entry.Type) == "TXT" && deref(entry.Name) == fqdn { return } } t.Fatalf("Could not find a TXT record for _acme-challenge.%s", domain) } + +func deref[T string | int | int32 | int64 | bool](v *T) T { + if v == nil { + var zero T + return zero + } + + return *v +} diff --git a/providers/dns/lightsail/lightsail_test.go b/providers/dns/lightsail/lightsail_test.go index a6b46045e..8ff60c11e 100644 --- a/providers/dns/lightsail/lightsail_test.go +++ b/providers/dns/lightsail/lightsail_test.go @@ -1,7 +1,7 @@ package lightsail import ( - "net/http/httptest" + "context" "os" "testing" @@ -10,7 +10,6 @@ import ( "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/lightsail" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -32,16 +31,37 @@ var envTest = tester.NewEnvTest( WithDomain(EnvDNSZone). WithLiveTestRequirements(envAwsAccessKeyID, envAwsSecretAccessKey, EnvDNSZone) +type endpointResolverMock struct { + endpoint string +} + +func (e endpointResolverMock) ResolveEndpoint(_, _ string, _ ...interface{}) (aws.Endpoint, error) { + return aws.Endpoint{URL: e.endpoint}, nil +} + +func makeProvider(serverURL string) *DNSProvider { + config := aws.Config{ + Credentials: credentials.NewStaticCredentialsProvider("abc", "123", " "), + Region: "mock-region", + EndpointResolverWithOptions: endpointResolverMock{endpoint: serverURL}, + RetryMaxAttempts: 1, + } + + return &DNSProvider{ + client: lightsail.NewFromConfig(config), + config: NewDefaultConfig(), + } +} + func TestCredentialsFromEnv(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() _ = os.Setenv(envAwsAccessKeyID, "123") _ = os.Setenv(envAwsSecretAccessKey, "123") _ = os.Setenv(envAwsRegion, "us-east-1") - ctx := t.Context() + ctx := context.Background() cfg, err := awsconfig.LoadDefaultConfig(ctx) require.NoError(t, err) @@ -57,25 +77,17 @@ func TestCredentialsFromEnv(t *testing.T) { } func TestDNSProvider_Present(t *testing.T) { - provider := servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - return &DNSProvider{ - client: lightsail.NewFromConfig(aws.Config{ - HTTPClient: server.Client(), - Credentials: credentials.NewStaticCredentialsProvider("abc", "123", " "), - Region: "mock-region", - BaseEndpoint: aws.String(server.URL), - RetryMaxAttempts: 1, - }), - config: NewDefaultConfig(), - }, nil - }). - Route("POST /", nil). - Build(t) + mockResponses := map[string]MockResponse{ + "/": {StatusCode: 200, Body: ""}, + } + + serverURL := newMockServer(t, mockResponses) + + provider := makeProvider(serverURL) domain := "example.com" keyAuth := "123456d==" err := provider.Present(domain, "", keyAuth) - require.NoError(t, err) + require.NoError(t, err, "Expected Present to return no error") } diff --git a/providers/dns/lightsail/mock_server_test.go b/providers/dns/lightsail/mock_server_test.go new file mode 100644 index 000000000..385c80850 --- /dev/null +++ b/providers/dns/lightsail/mock_server_test.go @@ -0,0 +1,44 @@ +package lightsail + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// MockResponse represents a predefined response used by a mock server. +type MockResponse struct { + StatusCode int + Body string +} + +func newMockServer(t *testing.T, responses map[string]MockResponse) string { + t.Helper() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + resp, ok := responses[path] + if !ok { + msg := fmt.Sprintf("Requested path not found in response map: %s", path) + require.FailNow(t, msg) + } + + w.Header().Set("Content-Type", "application/xml") + w.WriteHeader(resp.StatusCode) + _, err := w.Write([]byte(resp.Body)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + })) + + t.Cleanup(server.Close) + + time.Sleep(100 * time.Millisecond) + + return server.URL +} diff --git a/providers/dns/limacity/internal/client.go b/providers/dns/limacity/internal/client.go deleted file mode 100644 index ae6ab87eb..000000000 --- a/providers/dns/limacity/internal/client.go +++ /dev/null @@ -1,192 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" -) - -const defaultBaseURL = "https://www.lima-city.de/usercp" - -type Client struct { - apiKey string - baseURL *url.URL - HTTPClient *http.Client -} - -func NewClient(apiKey string) *Client { - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - apiKey: apiKey, - baseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - } -} - -func (c *Client) GetDomains(ctx context.Context) ([]Domain, error) { - endpoint := c.baseURL.JoinPath("domains.json") - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var results DomainsResponse - - err = c.do(req, &results) - if err != nil { - return nil, err - } - - return results.Data, nil -} - -func (c *Client) GetRecords(ctx context.Context, domainID int) ([]Record, error) { - endpoint := c.baseURL.JoinPath("domains", strconv.Itoa(domainID), "records.json") - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var results RecordsResponse - - err = c.do(req, &results) - if err != nil { - return nil, err - } - - return results.Data, nil -} - -func (c *Client) AddRecord(ctx context.Context, domainID int, record Record) error { - endpoint := c.baseURL.JoinPath("domains", strconv.Itoa(domainID), "records.json") - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, NameserverRecordPayload{Data: record}) - if err != nil { - return err - } - - var results APIResponse - - err = c.do(req, &results) - if err != nil { - return err - } - - return nil -} - -func (c *Client) UpdateRecord(ctx context.Context, domainID, recordID int, record Record) error { - endpoint := c.baseURL.JoinPath("domains", strconv.Itoa(domainID), "records", strconv.Itoa(recordID)) - - req, err := newJSONRequest(ctx, http.MethodPut, endpoint, NameserverRecordPayload{Data: record}) - if err != nil { - return err - } - - var results APIResponse - - err = c.do(req, &results) - if err != nil { - return err - } - - return nil -} - -func (c *Client) DeleteRecord(ctx context.Context, domainID, recordID int) error { - // /domains/{domainId}/records/{recordId} DELETE - endpoint := c.baseURL.JoinPath("domains", strconv.Itoa(domainID), "records", strconv.Itoa(recordID)) - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return err - } - - var results APIResponse - - err = c.do(req, &results) - if err != nil { - return err - } - - return nil -} - -func (c *Client) do(req *http.Request, result any) error { - req.SetBasicAuth("api", c.apiKey) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIResponse - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return fmt.Errorf("[status code: %d] %w", resp.StatusCode, &errAPI) -} diff --git a/providers/dns/limacity/internal/client_test.go b/providers/dns/limacity/internal/client_test.go deleted file mode 100644 index c43f12ba2..000000000 --- a/providers/dns/limacity/internal/client_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const apiKey = "secret" - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient(apiKey) - client.baseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithBasicAuth("api", apiKey), - ) -} - -func TestClient_GetDomains(t *testing.T) { - client := mockBuilder(). - Route("GET /domains.json", servermock.ResponseFromFixture("get-domains.json")). - Build(t) - - domains, err := client.GetDomains(t.Context()) - require.NoError(t, err) - - expected := []Domain{{ - ID: 123, - UnicodeFqdn: "example.com", - Domain: "example", - TLD: "com", - Status: "ok", - }} - assert.Equal(t, expected, domains) -} - -func TestClient_GetDomains_error(t *testing.T) { - client := mockBuilder(). - Route("GET /domains.json", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) - - _, err := client.GetDomains(t.Context()) - require.EqualError(t, err, "[status code: 400] status: invalid_resource, details: name: [muss ausgefüllt werden]") -} - -func TestClient_GetRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /domains/123/records.json", servermock.ResponseFromFixture("get-records.json")). - Build(t) - - records, err := client.GetRecords(t.Context(), 123) - require.NoError(t, err) - - expected := []Record{ - { - ID: 1234, - Content: "ns1.lima-city.de", - Name: "example.com", - TTL: 36000, - Type: "NS", - }, - { - ID: 5678, - Content: `"foobar"`, - Name: "_acme-challenge.example.com", - TTL: 36000, - Type: "TXT", - }, - } - assert.Equal(t, expected, records) -} - -func TestClient_GetRecords_error(t *testing.T) { - client := mockBuilder(). - Route("GET /domains/123/records.json", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) - - _, err := client.GetRecords(t.Context(), 123) - require.EqualError(t, err, "[status code: 400] status: invalid_resource, details: name: [muss ausgefüllt werden]") -} - -func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/123/records.json", - servermock.ResponseFromFixture("ok.json"), - servermock.CheckRequestJSONBody(`{"nameserver_record":{"name":"foo","content":"bar","ttl":12,"type":"TXT"}}`)). - Build(t) - - record := Record{ - Name: "foo", - Content: "bar", - TTL: 12, - Type: "TXT", - } - - err := client.AddRecord(t.Context(), 123, record) - require.NoError(t, err) -} - -func TestClient_AddRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/123/records.json", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) - - record := Record{ - Name: "foo", - Content: "bar", - TTL: 12, - Type: "TXT", - } - - err := client.AddRecord(t.Context(), 123, record) - require.EqualError(t, err, "[status code: 400] status: invalid_resource, details: name: [muss ausgefüllt werden]") -} - -func TestClient_UpdateRecord(t *testing.T) { - client := mockBuilder(). - Route("PUT /domains/123/records/456", - servermock.ResponseFromFixture("ok.json"), - servermock.CheckRequestJSONBody(`{"nameserver_record":{}}`)). - Build(t) - - err := client.UpdateRecord(t.Context(), 123, 456, Record{}) - require.NoError(t, err) -} - -func TestClient_UpdateRecord_error(t *testing.T) { - client := mockBuilder(). - Route("PUT /domains/123/records/456", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) - - err := client.UpdateRecord(t.Context(), 123, 456, Record{}) - require.EqualError(t, err, "[status code: 400] status: invalid_resource, details: name: [muss ausgefüllt werden]") -} - -func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /domains/123/records/456", - servermock.ResponseFromFixture("ok.json")). - Build(t) - - err := client.DeleteRecord(t.Context(), 123, 456) - require.NoError(t, err) -} - -func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /domains/123/records/456", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) - - err := client.DeleteRecord(t.Context(), 123, 456) - require.EqualError(t, err, "[status code: 400] status: invalid_resource, details: name: [muss ausgefüllt werden]") -} diff --git a/providers/dns/limacity/internal/fixtures/error.json b/providers/dns/limacity/internal/fixtures/error.json deleted file mode 100644 index 99dee169c..000000000 --- a/providers/dns/limacity/internal/fixtures/error.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "status": "invalid_resource", - "errors": { - "name": [ - "muss ausgefüllt werden" - ] - } -} diff --git a/providers/dns/limacity/internal/fixtures/get-domains.json b/providers/dns/limacity/internal/fixtures/get-domains.json deleted file mode 100644 index 1643a1766..000000000 --- a/providers/dns/limacity/internal/fixtures/get-domains.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "domains": [ - { - "id": 123, - "mode": "CREATE", - "tld": "com", - "domain": "example", - "in_subscription": false, - "auto_renew": false, - "status": "ok", - "unicode_fqdn": "example.com", - "registered_at": "1970-01-01T00:00:00+00:00", - "registered_until": "2000-01-01T00:00:00+00:00" - } - ] -} diff --git a/providers/dns/limacity/internal/fixtures/get-records.json b/providers/dns/limacity/internal/fixtures/get-records.json deleted file mode 100644 index 10f543464..000000000 --- a/providers/dns/limacity/internal/fixtures/get-records.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "records": [ - { - "id": 1234, - "content": "ns1.lima-city.de", - "name": "example.com", - "ttl": 36000, - "type": "NS", - "priority": null - }, - { - "id": 5678, - "content": "\"foobar\"", - "name": "_acme-challenge.example.com", - "subdomain": "_acme-challenge", - "ttl": 36000, - "type": "TXT", - "priority": null - } - ] -} diff --git a/providers/dns/limacity/internal/fixtures/ok.json b/providers/dns/limacity/internal/fixtures/ok.json deleted file mode 100644 index bc4e01029..000000000 --- a/providers/dns/limacity/internal/fixtures/ok.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "status": "ok" -} diff --git a/providers/dns/limacity/internal/types.go b/providers/dns/limacity/internal/types.go deleted file mode 100644 index 7411632ea..000000000 --- a/providers/dns/limacity/internal/types.go +++ /dev/null @@ -1,48 +0,0 @@ -package internal - -import ( - "fmt" - "strings" -) - -type RecordsResponse struct { - Data []Record `json:"records,omitempty"` -} - -type NameserverRecordPayload struct { - Data Record `json:"nameserver_record"` -} - -type DomainsResponse struct { - Data []Domain `json:"domains,omitempty"` -} - -type APIResponse struct { - Status string `json:"status,omitempty"` - Details map[string][]string `json:"errors,omitempty"` -} - -func (a APIResponse) Error() string { - var details []string - for k, v := range a.Details { - details = append(details, fmt.Sprintf("%s: %s", k, v)) - } - - return fmt.Sprintf("status: %s, details: %s", a.Status, strings.Join(details, ",")) -} - -type Record struct { - ID int `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Content string `json:"content,omitempty"` - TTL int `json:"ttl,omitempty"` - Type string `json:"type,omitempty"` -} - -type Domain struct { - ID int `json:"id,omitempty"` - UnicodeFqdn string `json:"unicode_fqdn,omitempty"` - Domain string `json:"domain,omitempty"` - TLD string `json:"tld,omitempty"` - Status string `json:"status,omitempty"` -} diff --git a/providers/dns/limacity/limacity.go b/providers/dns/limacity/limacity.go deleted file mode 100644 index 3291faf66..000000000 --- a/providers/dns/limacity/limacity.go +++ /dev/null @@ -1,215 +0,0 @@ -// Package limacity implements a DNS provider for solving the DNS-01 challenge using Lima-City DNS. -package limacity - -import ( - "context" - "errors" - "fmt" - "net/http" - "strconv" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/limacity/internal" -) - -// Environment variables names. -const ( - envNamespace = "LIMACITY_" - - EnvAPIKey = envNamespace + "API_KEY" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" - EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - TTL int - PropagationTimeout time.Duration - PollingInterval time.Duration - SequenceInterval time.Duration - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, 60), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 8*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 80*time.Second), - SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, 90*time.Second), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client - - domainIDs map[string]int - domainIDsMu sync.Mutex -} - -// NewDNSProvider returns a DNSProvider instance configured for Lima-City DNS. -// LIMACITY_API_KEY must be passed in the environment variables. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIKey) - if err != nil { - return nil, fmt.Errorf("limacity: %w", err) - } - - config := NewDefaultConfig() - config.APIKey = values[EnvAPIKey] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Lima-City DNS. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("limacity: the configuration of the DNS provider is nil") - } - - if config.APIKey == "" { - return nil, errors.New("limacity: APIKey is missing") - } - - client := internal.NewClient(config.APIKey) - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - domainIDs: make(map[string]int), - }, nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -// Sequential All DNS challenges for this provider will be resolved sequentially. -// Returns the interval between each iteration. -func (d *DNSProvider) Sequential() time.Duration { - return d.config.SequenceInterval -} - -// Present creates a TXT record to fulfill the dns-01 challenge. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - domains, err := d.client.GetDomains(ctx) - if err != nil { - return fmt.Errorf("limacity: get domains: %w", err) - } - - dom, err := findDomain(domains, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("limacity: find domain: %w", err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, dom.UnicodeFqdn) - if err != nil { - return fmt.Errorf("limacity: %w", err) - } - - record := internal.Record{ - Name: subDomain, - Content: info.Value, - TTL: d.config.TTL, - Type: "TXT", - } - - err = d.client.AddRecord(ctx, dom.ID, record) - if err != nil { - return fmt.Errorf("limacity: add record: %w", err) - } - - d.domainIDsMu.Lock() - d.domainIDs[token] = dom.ID - d.domainIDsMu.Unlock() - - return nil -} - -// CleanUp removes the TXT record. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - // gets the domain's unique ID - d.domainIDsMu.Lock() - domainID, ok := d.domainIDs[token] - d.domainIDsMu.Unlock() - - if !ok { - return fmt.Errorf("limacity: unknown domain ID for '%s' '%s'", info.EffectiveFQDN, token) - } - - records, err := d.client.GetRecords(ctx, domainID) - if err != nil { - return fmt.Errorf("limacity: get records: %w", err) - } - - var recordID int - - for _, record := range records { - if record.Type == "TXT" && record.Content == strconv.Quote(info.Value) { - recordID = record.ID - break - } - } - - if recordID == 0 { - return errors.New("limacity: TXT record not found") - } - - err = d.client.DeleteRecord(ctx, domainID, recordID) - if err != nil { - return fmt.Errorf("limacity: delete record (domain ID=%d, record ID=%d): %w", domainID, recordID, err) - } - - d.domainIDsMu.Lock() - delete(d.domainIDs, info.EffectiveFQDN) - d.domainIDsMu.Unlock() - - return nil -} - -func findDomain(domains []internal.Domain, fqdn string) (internal.Domain, error) { - for f := range dns01.DomainsSeq(fqdn) { - domain := dns01.UnFqdn(f) - - for _, dom := range domains { - if dom.UnicodeFqdn == domain || dom.UnicodeFqdn == f { - return dom, nil - } - } - } - - return internal.Domain{}, fmt.Errorf("domain %s not found", fqdn) -} diff --git a/providers/dns/limacity/limacity.toml b/providers/dns/limacity/limacity.toml deleted file mode 100644 index d236577d0..000000000 --- a/providers/dns/limacity/limacity.toml +++ /dev/null @@ -1,23 +0,0 @@ -Name = "Lima-City" -Description = '''''' -URL = "https://www.lima-city.de" -Code = "limacity" -Since = "v4.18.0" - -Example = ''' -LIMACITY_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns limacity -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - LIMACITY_API_KEY = "The API key" - [Configuration.Additional] - LIMACITY_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 80)" - LIMACITY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 480)" - LIMACITY_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 90)" - LIMACITY_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" - LIMACITY_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://www.lima-city.de/hilfe/lima-city-api" diff --git a/providers/dns/limacity/limacity_test.go b/providers/dns/limacity/limacity_test.go deleted file mode 100644 index 3301fcb2e..000000000 --- a/providers/dns/limacity/limacity_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package limacity - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAPIKey: "key", - }, - }, - { - desc: "missing API key", - envVars: map[string]string{}, - expected: "limacity: some credentials information are missing: LIMACITY_API_KEY", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiKey string - expected string - }{ - { - desc: "success", - apiKey: "key", - }, - { - desc: "missing API key", - expected: "limacity: APIKey is missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIKey = test.apiKey - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/linode/linode.go b/providers/dns/linode/linode.go index b03dee4f5..54af31e2e 100644 --- a/providers/dns/linode/linode.go +++ b/providers/dns/linode/linode.go @@ -9,15 +9,18 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" "github.com/linode/linodego" "golang.org/x/oauth2" ) +const ( + minTTL = 300 + dnsUpdateFreqMins = 15 + dnsUpdateFudgeSecs = 120 +) + // Environment variables names. const ( envNamespace = "LINODE_" @@ -30,14 +33,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const ( - minTTL = 300 - dnsUpdateFreqMins = 15 - dnsUpdateFudgeSecs = 120 -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Token string @@ -51,9 +46,9 @@ type Config struct { func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, minTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 0), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 15*time.Second), - HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 0), } } @@ -103,8 +98,8 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { }, } - client := linodego.NewClient(clientdebug.Wrap(oauth2Client)) - client.SetUserAgent(useragent.Get()) + client := linodego.NewClient(oauth2Client) + client.SetUserAgent("go-acme/lego https://github.com/linode/linodego") return &DNSProvider{config: config, client: &client}, nil } @@ -131,11 +126,9 @@ func (d *DNSProvider) Timeout() (time.Duration, time.Duration) { // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) - zone, err := d.getHostedZoneInfo(ctx, info.EffectiveFQDN) + zone, err := d.getHostedZoneInfo(info.EffectiveFQDN) if err != nil { return err } @@ -147,26 +140,22 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { Type: linodego.RecordTypeTXT, } - _, err = d.client.CreateDomainRecord(ctx, zone.domainID, createOpts) - + _, err = d.client.CreateDomainRecord(context.Background(), zone.domainID, createOpts) return err } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) - zone, err := d.getHostedZoneInfo(ctx, info.EffectiveFQDN) + zone, err := d.getHostedZoneInfo(info.EffectiveFQDN) if err != nil { return err } // Get all TXT records for the specified domain. listOpts := linodego.NewListOptions(0, `{"type":"TXT"}`) - - resources, err := d.client.ListDomainRecords(ctx, zone.domainID, listOpts) + resources, err := d.client.ListDomainRecords(context.Background(), zone.domainID, listOpts) if err != nil { return err } @@ -175,7 +164,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { for _, resource := range resources { if (resource.Name == dns01.UnFqdn(info.EffectiveFQDN) || resource.Name == zone.resourceName) && resource.Target == info.Value { - if err := d.client.DeleteDomainRecord(ctx, zone.domainID, resource.ID); err != nil { + if err := d.client.DeleteDomainRecord(context.Background(), zone.domainID, resource.ID); err != nil { return err } } @@ -184,7 +173,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return nil } -func (d *DNSProvider) getHostedZoneInfo(ctx context.Context, fqdn string) (*hostedZoneInfo, error) { +func (d *DNSProvider) getHostedZoneInfo(fqdn string) (*hostedZoneInfo, error) { // Lookup the zone that handles the specified FQDN. authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { @@ -198,8 +187,7 @@ func (d *DNSProvider) getHostedZoneInfo(ctx context.Context, fqdn string) (*host } listOpts := linodego.NewListOptions(0, string(filter)) - - domains, err := d.client.ListDomains(ctx, listOpts) + domains, err := d.client.ListDomains(context.Background(), listOpts) if err != nil { return nil, err } diff --git a/providers/dns/linode/linode.toml b/providers/dns/linode/linode.toml index 9ea30b92b..2910916ef 100644 --- a/providers/dns/linode/linode.toml +++ b/providers/dns/linode/linode.toml @@ -2,22 +2,21 @@ Name = "Linode (v4)" Description = '''''' URL = "https://www.linode.com/" Code = "linode" -Aliases = ["linodev4"] # "linodev4" is for compatibility with v3, must be dropped in v5 Since = "v1.1.0" Example = ''' LINODE_TOKEN=xxxxx \ -lego --dns linode -d '*.example.com' -d example.com run +lego --email you@example.com --dns linode --domains my.example.org run ''' [Configuration] [Configuration.Credentials] LINODE_TOKEN = "API token" [Configuration.Additional] - LINODE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 15)" - LINODE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - LINODE_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - LINODE_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + LINODE_POLLING_INTERVAL = "Time between DNS propagation check" + LINODE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + LINODE_TTL = "The TTL of the TXT record used for the DNS challenge" + LINODE_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://developers.linode.com/api/v4" diff --git a/providers/dns/linode/linode_test.go b/providers/dns/linode/linode_test.go index 1c4903aca..70b33eda4 100644 --- a/providers/dns/linode/linode_test.go +++ b/providers/dns/linode/linode_test.go @@ -1,20 +1,69 @@ package linode import ( + "encoding/json" + "fmt" "net/http" "net/http/httptest" "os" "testing" + "time" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/linode/linodego" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +type MockResponseMap map[string]interface{} + var envTest = tester.NewEnvTest(EnvToken) +func setupTest(t *testing.T, responses MockResponseMap) string { + t.Helper() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Ensure that we support the requested action. + action := r.Method + ":" + r.URL.Path + resp, ok := responses[action] + if !ok { + http.Error(w, fmt.Sprintf("Unsupported mock action: %q", action), http.StatusInternalServerError) + return + } + + rawResponse, err := json.Marshal(resp) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to JSON encode response: %v", err), http.StatusInternalServerError) + return + } + + // Send the response. + w.Header().Set("Content-Type", "application/json") + if err, ok := resp.(linodego.APIError); ok { + if err.Errors[0].Reason == "Not found" { + w.WriteHeader(http.StatusNotFound) + } else { + w.WriteHeader(http.StatusBadRequest) + } + } else { + w.WriteHeader(http.StatusOK) + } + + _, err = w.Write(rawResponse) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + server := httptest.NewServer(handler) + t.Cleanup(server.Close) + + time.Sleep(100 * time.Millisecond) + + return server.URL +} + func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string @@ -39,7 +88,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -95,80 +143,83 @@ func TestNewDNSProviderConfig(t *testing.T) { func TestDNSProvider_Present(t *testing.T) { defer envTest.RestoreEnv() - os.Setenv(EnvToken, "testing") + p, err := NewDNSProvider() + require.NoError(t, err) + require.NotNil(t, p) + domain := "example.com" keyAuth := "dGVzdGluZw==" testCases := []struct { desc string - builder *servermock.Builder[*DNSProvider] + mockResponses MockResponseMap expectedError string }{ { desc: "Success", - builder: mockBuilder(). - Route("GET /v4/domains", - servermock.JSONEncode(linodego.DomainsPagedResponse{ - PageOptions: &linodego.PageOptions{ - Pages: 1, - Results: 1, - Page: 1, - }, - Data: []linodego.Domain{{ - Domain: domain, - ID: 1234, - }}, - })). - Route("POST /v4/domains/1234/records", servermock.JSONEncode(linodego.DomainRecord{ + mockResponses: MockResponseMap{ + "GET:/v4/domains": linodego.DomainsPagedResponse{ + PageOptions: &linodego.PageOptions{ + Pages: 1, + Results: 1, + Page: 1, + }, + Data: []linodego.Domain{{ + Domain: domain, + ID: 1234, + }}, + }, + "POST:/v4/domains/1234/records": linodego.DomainRecord{ ID: 1234, - })), + }, + }, }, { desc: "NoDomain", - builder: mockBuilder(). - Route("GET /v4/domains", - servermock.JSONEncode(linodego.APIError{ - Errors: []linodego.APIErrorReason{{ - Reason: "Not found", - }}, - }). - WithStatusCode(http.StatusNotFound)), + mockResponses: MockResponseMap{ + "GET:/v4/domains": linodego.APIError{ + Errors: []linodego.APIErrorReason{{ + Reason: "Not found", + }}, + }, + }, expectedError: "[404] Not found", }, { desc: "CreateFailed", - builder: mockBuilder(). - Route("GET /v4/domains", - servermock.JSONEncode(&linodego.DomainsPagedResponse{ - PageOptions: &linodego.PageOptions{ - Pages: 1, - Results: 1, - Page: 1, - }, - Data: []linodego.Domain{{ - Domain: "example.com", - ID: 1234, - }}, - })). - Route("POST /v4/domains/1234/records", - servermock.JSONEncode(linodego.APIError{ - Errors: []linodego.APIErrorReason{{ - Reason: "Failed to create domain resource", - Field: "somefield", - }}, - }). - WithStatusCode(http.StatusBadRequest)), + mockResponses: MockResponseMap{ + "GET:/v4/domains": &linodego.DomainsPagedResponse{ + PageOptions: &linodego.PageOptions{ + Pages: 1, + Results: 1, + Page: 1, + }, + Data: []linodego.Domain{{ + Domain: "example.com", + ID: 1234, + }}, + }, + "POST:/v4/domains/1234/records": linodego.APIError{ + Errors: []linodego.APIErrorReason{{ + Reason: "Failed to create domain resource", + Field: "somefield", + }}, + }, + }, expectedError: "[400] [somefield] Failed to create domain resource", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - provider := test.builder.Build(t) + serverURL := setupTest(t, test.mockResponses) - err := provider.Present(domain, "", keyAuth) + assert.NotNil(t, p.client) + p.client.SetBaseURL(serverURL) + + err = p.Present(domain, "", keyAuth) if test.expectedError == "" { assert.NoError(t, err) } else { @@ -180,114 +231,109 @@ func TestDNSProvider_Present(t *testing.T) { func TestDNSProvider_CleanUp(t *testing.T) { defer envTest.RestoreEnv() - os.Setenv(EnvToken, "testing") + p, err := NewDNSProvider() + require.NoError(t, err) + domain := "example.com" keyAuth := "dGVzdGluZw==" testCases := []struct { desc string - builder *servermock.Builder[*DNSProvider] + mockResponses MockResponseMap expectedError string }{ { desc: "Success", - builder: mockBuilder(). - Route("GET /v4/domains", - servermock.JSONEncode(&linodego.DomainsPagedResponse{ - PageOptions: &linodego.PageOptions{ - Pages: 1, - Results: 1, - Page: 1, - }, - Data: []linodego.Domain{{ - Domain: "foobar.com", - ID: 1234, - }}, - })). - Route("GET /v4/domains/1234/records", - servermock.JSONEncode(&linodego.DomainRecordsPagedResponse{ - PageOptions: &linodego.PageOptions{ - Pages: 1, - Results: 1, - Page: 1, - }, - Data: []linodego.DomainRecord{{ - ID: 1234, - Name: "_acme-challenge", - Target: "ElbOJKOkFWiZLQeoxf-wb3IpOsQCdvoM0y_wn0TEkxM", - Type: "TXT", - }}, - })). - Route("DELETE /v4/domains/1234/records/1234", - servermock.RawStringResponse("{}").WithHeader("Content-Type", "application/json")), + mockResponses: MockResponseMap{ + "GET:/v4/domains": &linodego.DomainsPagedResponse{ + PageOptions: &linodego.PageOptions{ + Pages: 1, + Results: 1, + Page: 1, + }, + Data: []linodego.Domain{{ + Domain: "foobar.com", + ID: 1234, + }}, + }, + "GET:/v4/domains/1234/records": &linodego.DomainRecordsPagedResponse{ + PageOptions: &linodego.PageOptions{ + Pages: 1, + Results: 1, + Page: 1, + }, + Data: []linodego.DomainRecord{{ + ID: 1234, + Name: "_acme-challenge", + Target: "ElbOJKOkFWiZLQeoxf-wb3IpOsQCdvoM0y_wn0TEkxM", + Type: "TXT", + }}, + }, + "DELETE:/v4/domains/1234/records/1234": struct{}{}, + }, }, { desc: "NoDomain", - builder: mockBuilder(). - Route("GET /v4/domains", - servermock.JSONEncode(linodego.APIError{ - Errors: []linodego.APIErrorReason{{ - Reason: "Not found", - }}, - }). - WithStatusCode(http.StatusNotFound)). - Route("GET /v4/domains/1234/records", - servermock.JSONEncode(linodego.APIError{ - Errors: []linodego.APIErrorReason{{ - Reason: "Not found", - }}, - }, - ). - WithStatusCode(http.StatusNotFound)), + mockResponses: MockResponseMap{ + "GET:/v4/domains": linodego.APIError{ + Errors: []linodego.APIErrorReason{{ + Reason: "Not found", + }}, + }, + "GET:/v4/domains/1234/records": linodego.APIError{ + Errors: []linodego.APIErrorReason{{ + Reason: "Not found", + }}, + }, + }, expectedError: "[404] Not found", }, { desc: "DeleteFailed", - builder: mockBuilder(). - Route("GET /v4/domains", - servermock.JSONEncode(linodego.DomainsPagedResponse{ - PageOptions: &linodego.PageOptions{ - Pages: 1, - Results: 1, - Page: 1, - }, - Data: []linodego.Domain{{ - ID: 1234, - Domain: "example.com", - }}, - })). - Route("GET /v4/domains/1234/records", - servermock.JSONEncode(linodego.DomainRecordsPagedResponse{ - PageOptions: &linodego.PageOptions{ - Pages: 1, - Results: 1, - Page: 1, - }, - Data: []linodego.DomainRecord{{ - ID: 1234, - Name: "_acme-challenge", - Target: "ElbOJKOkFWiZLQeoxf-wb3IpOsQCdvoM0y_wn0TEkxM", - Type: "TXT", - }}, - })). - Route("DELETE /v4/domains/1234/records/1234", - servermock.JSONEncode(linodego.APIError{ - Errors: []linodego.APIErrorReason{{ - Reason: "Failed to delete domain resource", - }}, - }). - WithStatusCode(http.StatusBadRequest)), + mockResponses: MockResponseMap{ + "GET:/v4/domains": linodego.DomainsPagedResponse{ + PageOptions: &linodego.PageOptions{ + Pages: 1, + Results: 1, + Page: 1, + }, + Data: []linodego.Domain{{ + ID: 1234, + Domain: "example.com", + }}, + }, + "GET:/v4/domains/1234/records": linodego.DomainRecordsPagedResponse{ + PageOptions: &linodego.PageOptions{ + Pages: 1, + Results: 1, + Page: 1, + }, + Data: []linodego.DomainRecord{{ + ID: 1234, + Name: "_acme-challenge", + Target: "ElbOJKOkFWiZLQeoxf-wb3IpOsQCdvoM0y_wn0TEkxM", + Type: "TXT", + }}, + }, + "DELETE:/v4/domains/1234/records/1234": linodego.APIError{ + Errors: []linodego.APIErrorReason{{ + Reason: "Failed to delete domain resource", + }}, + }, + }, expectedError: "[400] Failed to delete domain resource", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - provider := test.builder.Build(t) + serverURL := setupTest(t, test.mockResponses) - err := provider.CleanUp(domain, "", keyAuth) + p.client.SetBaseURL(serverURL) + + err = p.CleanUp(domain, "", keyAuth) if test.expectedError == "" { assert.NoError(t, err) } else { @@ -310,16 +356,3 @@ func TestLiveCleanUp(t *testing.T) { } // TODO implement this test } - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) { - p, err := NewDNSProvider() - if err != nil { - return nil, err - } - - p.client.SetBaseURL(server.URL) - - return p, nil - }) -} diff --git a/providers/dns/liquidweb/liquidweb.go b/providers/dns/liquidweb/liquidweb.go index 6e93e2a12..c7fd9eeb7 100644 --- a/providers/dns/liquidweb/liquidweb.go +++ b/providers/dns/liquidweb/liquidweb.go @@ -10,13 +10,14 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" lw "github.com/liquidweb/liquidweb-go/client" "github.com/liquidweb/liquidweb-go/network" ) +const defaultBaseURL = "https://api.liquidweb.com" + // Environment variables names. const ( envNamespace = "LIQUID_WEB_" @@ -33,10 +34,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const defaultBaseURL = "https://api.liquidweb.com" - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string @@ -55,16 +52,15 @@ func NewDefaultConfig() *Config { BaseURL: defaultBaseURL, TTL: env.GetOneWithFallback(EnvTTL, 300, strconv.Atoi, altEnvName(EnvTTL)), PropagationTimeout: env.GetOneWithFallback(EnvPropagationTimeout, 2*time.Minute, env.ParseSecond, altEnvName(EnvPropagationTimeout)), - PollingInterval: env.GetOneWithFallback(EnvPollingInterval, dns01.DefaultPollingInterval, env.ParseSecond, altEnvName(EnvPollingInterval)), + PollingInterval: env.GetOneWithFallback(EnvPollingInterval, 2*time.Second, env.ParseSecond, altEnvName(EnvPollingInterval)), HTTPTimeout: env.GetOneWithFallback(EnvHTTPTimeout, 1*time.Minute, env.ParseSecond, altEnvName(EnvHTTPTimeout)), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - config *Config - client *lw.API - + config *Config + client *lw.API recordIDs map[string]int recordIDsMu sync.Mutex } @@ -160,7 +156,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } params := &network.DNSRecordParams{ID: recordID} - _, err := d.client.NetworkDNS.Delete(params) if err != nil { return fmt.Errorf("liquidweb: could not remove TXT record: %w", err) @@ -181,7 +176,6 @@ func (d *DNSProvider) findZone(domain string) (string, error) { // filter the zones on the account to only ones that match var zs []network.DNSZone - for _, item := range zones.Items { if strings.HasSuffix(domain, item.Name) { zs = append(zs, item) diff --git a/providers/dns/liquidweb/liquidweb.toml b/providers/dns/liquidweb/liquidweb.toml index 386b99cab..c9116912e 100644 --- a/providers/dns/liquidweb/liquidweb.toml +++ b/providers/dns/liquidweb/liquidweb.toml @@ -7,7 +7,7 @@ Since = "v3.1.0" Example = ''' LWAPI_USERNAME=someuser \ LWAPI_PASSWORD=somepass \ -lego --dns liquidweb -d '*.example.com' -d example.com run +lego --email you@example.com --dns liquidweb --domains my.example.org run ''' [Configuration] @@ -17,10 +17,10 @@ lego --dns liquidweb -d '*.example.com' -d example.com run [Configuration.Additional] LWAPI_ZONE = "DNS Zone" LWAPI_URL = "Liquid Web API endpoint" - LWAPI_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - LWAPI_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - LWAPI_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - LWAPI_HTTP_TIMEOUT = "API request timeout in seconds (Default: 60)" + LWAPI_TTL = "The TTL of the TXT record used for the DNS challenge" + LWAPI_POLLING_INTERVAL = "Time between DNS propagation check" + LWAPI_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + LWAPI_HTTP_TIMEOUT = "Maximum waiting time for the DNS records to be created (not verified)" [Links] API = "https://api.liquidweb.com/docs/" diff --git a/providers/dns/liquidweb/liquidweb_test.go b/providers/dns/liquidweb/liquidweb_test.go index a34d19037..a26b18e1b 100644 --- a/providers/dns/liquidweb/liquidweb_test.go +++ b/providers/dns/liquidweb/liquidweb_test.go @@ -18,6 +18,22 @@ var envTest = tester.NewEnvTest( EnvZone). WithDomain(envDomain) +func setupTest(t *testing.T, initRecs ...network.DNSRecord) *DNSProvider { + t.Helper() + + serverURL := mockAPIServer(t, initRecs) + + config := NewDefaultConfig() + config.Username = "blars" + config.Password = "tacoman" + config.BaseURL = serverURL + + provider, err := NewDNSProviderConfig(config) + require.NoError(t, err) + + return provider +} + func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string @@ -27,16 +43,16 @@ func TestNewDNSProvider(t *testing.T) { { desc: "minimum-success", envVars: map[string]string{ - EnvUsername: "user", - EnvPassword: "secret", + EnvUsername: "blars", + EnvPassword: "tacoman", }, }, { desc: "set-everything", envVars: map[string]string{ - EnvURL: "https://storm.example", - EnvUsername: "user", - EnvPassword: "secret", + EnvURL: "https://storm.com", + EnvUsername: "blars", + EnvPassword: "tacoman", EnvZone: "blars.com", }, }, @@ -48,16 +64,16 @@ func TestNewDNSProvider(t *testing.T) { { desc: "missing username", envVars: map[string]string{ - EnvPassword: "secret", - EnvZone: "blars.example", + EnvPassword: "tacoman", + EnvZone: "blars.com", }, expected: "liquidweb: some credentials information are missing: LIQUID_WEB_USERNAME", }, { desc: "missing password", envVars: map[string]string{ - EnvUsername: "user", - EnvZone: "blars.example", + EnvUsername: "blars", + EnvZone: "blars.com", }, expected: "liquidweb: some credentials information are missing: LIQUID_WEB_PASSWORD", }, @@ -66,7 +82,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -146,15 +161,15 @@ func TestNewDNSProviderConfig(t *testing.T) { } func TestDNSProvider_Present(t *testing.T) { - provider := mockProvider(t) + provider := setupTest(t) - err := provider.Present("tacoman.example", "", "") + err := provider.Present("tacoman.com", "", "") require.NoError(t, err) } func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockProvider(t, network.DNSRecord{ - Name: "_acme-challenge.tacoman.example", + provider := setupTest(t, network.DNSRecord{ + Name: "_acme-challenge.tacoman.com", RData: "123d==", Type: "TXT", TTL: 300, @@ -164,7 +179,7 @@ func TestDNSProvider_CleanUp(t *testing.T) { provider.recordIDs["123d=="] = 1234567 - err := provider.CleanUp("tacoman.example.", "123d==", "") + err := provider.CleanUp("tacoman.com.", "123d==", "") require.NoError(t, err) } @@ -181,7 +196,7 @@ func TestDNSProvider(t *testing.T) { }{ { desc: "expected successful", - domain: "tacoman.example", + domain: "tacoman.com", token: "123", keyAuth: "456", present: true, @@ -189,7 +204,7 @@ func TestDNSProvider(t *testing.T) { }, { desc: "other successful", - domain: "banana.example", + domain: "banana.com", token: "123", keyAuth: "456", present: true, @@ -197,16 +212,16 @@ func TestDNSProvider(t *testing.T) { }, { desc: "zone not on account", - domain: "huckleberry.example", + domain: "huckleberry.com", token: "123", keyAuth: "456", present: true, - expPresentErr: "no valid zone in account for certificate '_acme-challenge.huckleberry.example'", + expPresentErr: "no valid zone in account for certificate '_acme-challenge.huckleberry.com'", cleanup: false, }, { desc: "ssl for domain", - domain: "sundae.cherry.example", + domain: "sundae.cherry.com", token: "5847953", keyAuth: "34872934", present: true, @@ -214,7 +229,7 @@ func TestDNSProvider(t *testing.T) { }, { desc: "complicated domain", - domain: "always.money.stand.banana.example", + domain: "always.money.stand.banana.com", token: "5847953", keyAuth: "there is always money in the banana stand", present: true, @@ -224,7 +239,7 @@ func TestDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - provider := mockProvider(t, test.initRecs...) + provider := setupTest(t, test.initRecs...) if test.present { err := provider.Present(test.domain, test.token, test.keyAuth) @@ -249,7 +264,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/liquidweb/servermock_test.go b/providers/dns/liquidweb/servermock_test.go index 4886e17f1..8c22595af 100644 --- a/providers/dns/liquidweb/servermock_test.go +++ b/providers/dns/liquidweb/servermock_test.go @@ -1,6 +1,7 @@ package liquidweb import ( + "bytes" "encoding/json" "fmt" "io" @@ -9,12 +10,11 @@ import ( "net/http/httptest" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/liquidweb/liquidweb-go/network" "github.com/liquidweb/liquidweb-go/types" ) -func mockProvider(t *testing.T, initRecs ...network.DNSRecord) *DNSProvider { +func mockAPIServer(t *testing.T, initRecs []network.DNSRecord) string { t.Helper() recs := make(map[int]network.DNSRecord) @@ -23,142 +23,157 @@ func mockProvider(t *testing.T, initRecs ...network.DNSRecord) *DNSProvider { recs[int(rec.ID)] = rec } - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.Username = "user" - config.Password = "secret" - config.BaseURL = server.URL + mux := http.NewServeMux() + mux.Handle("/v1/Network/DNS/Record/delete", mockAPIDelete(recs)) + mux.Handle("/v1/Network/DNS/Record/create", mockAPICreate(recs)) + mux.Handle("/v1/Network/DNS/Zone/list", mockAPIListZones()) + mux.Handle("/bleed/Network/DNS/Record/delete", mockAPIDelete(recs)) + mux.Handle("/bleed/Network/DNS/Record/create", mockAPICreate(recs)) + mux.Handle("/bleed/Network/DNS/Zone/list", mockAPIListZones()) - return NewDNSProviderConfig(config) - }, - servermock.CheckHeader(). - WithBasicAuth("user", "secret"), - ). - Route("/v1/Network/DNS/Record/delete", mockAPIDelete(recs)). - Route("/v1/Network/DNS/Record/create", mockAPICreate(recs)). - Route("/v1/Network/DNS/Zone/list", mockAPIListZones()). - Route("/bleed/Network/DNS/Record/delete", mockAPIDelete(recs)). - Route("/bleed/Network/DNS/Record/create", mockAPICreate(recs)). - Route("/bleed/Network/DNS/Zone/list", mockAPIListZones()). - Build(t) + server := httptest.NewServer(requireBasicAuth(requireJSON(mux))) + t.Cleanup(server.Close) + + return server.URL +} + +func requireBasicAuth(next http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + username, password, ok := r.BasicAuth() + if ok && username == "blars" && password == "tacoman" { + next.ServeHTTP(w, r) + return + } + + http.Error(w, "invalid auth", http.StatusForbidden) + } +} + +func requireJSON(next http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + buf := &bytes.Buffer{} + + _, err := buf.ReadFrom(r.Body) + if err != nil { + http.Error(w, "malformed request - json required", http.StatusBadRequest) + return + } + + r.Body = io.NopCloser(buf) + next.ServeHTTP(w, r) + } } func mockAPICreate(recs map[int]network.DNSRecord) http.HandlerFunc { _, mockAPIServerZones := makeMockZones() - return func(rw http.ResponseWriter, req *http.Request) { - body, err := io.ReadAll(req.Body) + return func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) if err != nil { - http.Error(rw, "invalid request", http.StatusInternalServerError) + http.Error(w, "invalid request", http.StatusInternalServerError) return } - payload := struct { + req := struct { Params network.DNSRecord `json:"params"` }{} - if err = json.Unmarshal(body, &payload); err != nil { - http.Error(rw, makeEncodingError(body), http.StatusBadRequest) + if err = json.Unmarshal(body, &req); err != nil { + http.Error(w, makeEncodingError(body), http.StatusBadRequest) return } + req.Params.ID = types.FlexInt(rand.Intn(10000000)) + req.Params.ZoneID = types.FlexInt(mockAPIServerZones[req.Params.Name]) - payload.Params.ID = types.FlexInt(rand.Intn(10000000)) - payload.Params.ZoneID = types.FlexInt(mockAPIServerZones[payload.Params.Name]) - - if _, exists := recs[int(payload.Params.ID)]; exists { - http.Error(rw, "dns record already exists", http.StatusTeapot) + if _, exists := recs[int(req.Params.ID)]; exists { + http.Error(w, "dns record already exists", http.StatusTeapot) return } + recs[int(req.Params.ID)] = req.Params - recs[int(payload.Params.ID)] = payload.Params - - resp, err := json.Marshal(payload.Params) + resp, err := json.Marshal(req.Params) if err != nil { - http.Error(rw, "", http.StatusInternalServerError) + http.Error(w, "", http.StatusInternalServerError) return } - - http.Error(rw, string(resp), http.StatusOK) + http.Error(w, string(resp), http.StatusOK) } } func mockAPIDelete(recs map[int]network.DNSRecord) http.HandlerFunc { - return func(rw http.ResponseWriter, req *http.Request) { - body, err := io.ReadAll(req.Body) + return func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) if err != nil { - http.Error(rw, "invalid request", http.StatusInternalServerError) + http.Error(w, "invalid request", http.StatusInternalServerError) return } - payload := struct { + req := struct { Params struct { Name string `json:"name"` ID int `json:"id"` } `json:"params"` }{} - if err := json.Unmarshal(body, &payload); err != nil { - http.Error(rw, makeEncodingError(body), http.StatusBadRequest) + if err := json.Unmarshal(body, &req); err != nil { + http.Error(w, makeEncodingError(body), http.StatusBadRequest) return } - if payload.Params.ID == 0 { - http.Error(rw, `{"error":"","error_class":"LW::Exception::Input::Multiple","errors":[{"error":"","error_class":"LW::Exception::Input::Required","field":"id","full_message":"The required field 'id' was missing a value.","position":null}],"field":["id"],"full_message":"The following input errors occurred:\nThe required field 'id' was missing a value.","type":null}`, http.StatusOK) + if req.Params.ID == 0 { + http.Error(w, `{"error":"","error_class":"LW::Exception::Input::Multiple","errors":[{"error":"","error_class":"LW::Exception::Input::Required","field":"id","full_message":"The required field 'id' was missing a value.","position":null}],"field":["id"],"full_message":"The following input errors occurred:\nThe required field 'id' was missing a value.","type":null}`, http.StatusOK) return } - if _, ok := recs[payload.Params.ID]; !ok { - http.Error(rw, fmt.Sprintf(`{"error":"","error_class":"LW::Exception::RecordNotFound","field":"network_dns_rr","full_message":"Record 'network_dns_rr: %d' not found","input":"%d","public_message":null}`, payload.Params.ID, payload.Params.ID), http.StatusOK) + if _, ok := recs[req.Params.ID]; !ok { + http.Error(w, fmt.Sprintf(`{"error":"","error_class":"LW::Exception::RecordNotFound","field":"network_dns_rr","full_message":"Record 'network_dns_rr: %d' not found","input":"%d","public_message":null}`, req.Params.ID, req.Params.ID), http.StatusOK) return } - - delete(recs, payload.Params.ID) - http.Error(rw, fmt.Sprintf("{\"deleted\":%d}", payload.Params.ID), http.StatusOK) + delete(recs, req.Params.ID) + http.Error(w, fmt.Sprintf("{\"deleted\":%d}", req.Params.ID), http.StatusOK) } } func mockAPIListZones() http.HandlerFunc { mockZones, mockAPIServerZones := makeMockZones() - return func(rw http.ResponseWriter, req *http.Request) { - body, err := io.ReadAll(req.Body) + return func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) if err != nil { - http.Error(rw, "invalid request", http.StatusInternalServerError) + http.Error(w, "invalid request", http.StatusInternalServerError) return } - payload := struct { + req := struct { Params struct { PageNum int `json:"page_num"` } `json:"params"` }{} - if err = json.Unmarshal(body, &payload); err != nil { - http.Error(rw, makeEncodingError(body), http.StatusBadRequest) + if err = json.Unmarshal(body, &req); err != nil { + http.Error(w, makeEncodingError(body), http.StatusBadRequest) return } switch { - case payload.Params.PageNum < 1: - payload.Params.PageNum = 1 - case payload.Params.PageNum > len(mockZones): - payload.Params.PageNum = len(mockZones) + case req.Params.PageNum < 1: + req.Params.PageNum = 1 + case req.Params.PageNum > len(mockZones): + req.Params.PageNum = len(mockZones) } - - resp := mockZones[payload.Params.PageNum] + resp := mockZones[req.Params.PageNum] resp.ItemTotal = types.FlexInt(len(mockAPIServerZones)) - resp.PageNum = types.FlexInt(payload.Params.PageNum) + resp.PageNum = types.FlexInt(req.Params.PageNum) resp.PageSize = 5 resp.PageTotal = types.FlexInt(len(mockZones)) var respBody []byte if respBody, err = json.Marshal(resp); err == nil { - http.Error(rw, string(respBody), http.StatusOK) + http.Error(w, string(respBody), http.StatusOK) return } - http.Error(rw, "", http.StatusInternalServerError) + http.Error(w, "", http.StatusInternalServerError) } } @@ -172,38 +187,38 @@ func makeMockZones() (map[int]network.DNSZoneList, map[string]int) { Items: []network.DNSZone{ { ID: 1, - Name: "blars.example", + Name: "blars.com", Active: 1, DelegationStatus: "CORRECT", - PrimaryNameserver: "ns.example.org", + PrimaryNameserver: "ns.liquidweb.com", }, { ID: 2, - Name: "tacoman.example", + Name: "tacoman.com", Active: 1, DelegationStatus: "CORRECT", - PrimaryNameserver: "ns.example.org", + PrimaryNameserver: "ns.liquidweb.com", }, { ID: 3, - Name: "storm.example", + Name: "storm.com", Active: 1, DelegationStatus: "CORRECT", - PrimaryNameserver: "ns.example.org", + PrimaryNameserver: "ns.liquidweb.com", }, { ID: 4, - Name: "not-apple.example", + Name: "not-apple.com", Active: 1, DelegationStatus: "BAD_NAMESERVERS", - PrimaryNameserver: "ns.example.org", + PrimaryNameserver: "ns.liquidweb.com", }, { ID: 5, Name: "example.com", Active: 1, DelegationStatus: "BAD_NAMESERVERS", - PrimaryNameserver: "ns.example.org", + PrimaryNameserver: "ns.liquidweb.com", }, }, }, @@ -211,38 +226,38 @@ func makeMockZones() (map[int]network.DNSZoneList, map[string]int) { Items: []network.DNSZone{ { ID: 6, - Name: "banana.example", + Name: "banana.com", Active: 1, DelegationStatus: "NXDOMAIN", - PrimaryNameserver: "ns.example.org", + PrimaryNameserver: "ns.liquidweb.com", }, { ID: 7, - Name: "cherry.example", + Name: "cherry.com", Active: 1, DelegationStatus: "SERVFAIL", - PrimaryNameserver: "ns.example.org", + PrimaryNameserver: "ns.liquidweb.com", }, { ID: 8, - Name: "dates.example", + Name: "dates.com", Active: 1, DelegationStatus: "SERVFAIL", - PrimaryNameserver: "ns.example.org", + PrimaryNameserver: "ns.liquidweb.com", }, { ID: 9, - Name: "eggplant.example", + Name: "eggplant.com", Active: 1, DelegationStatus: "SERVFAIL", - PrimaryNameserver: "ns.example.org", + PrimaryNameserver: "ns.liquidweb.com", }, { ID: 10, - Name: "fig.example", + Name: "fig.com", Active: 1, DelegationStatus: "UNKNOWN", - PrimaryNameserver: "ns.example.org", + PrimaryNameserver: "ns.liquidweb.com", }, }, }, @@ -250,43 +265,41 @@ func makeMockZones() (map[int]network.DNSZoneList, map[string]int) { Items: []network.DNSZone{ { ID: 11, - Name: "grapes.example", + Name: "grapes.com", Active: 1, DelegationStatus: "UNKNOWN", - PrimaryNameserver: "ns.example.org", + PrimaryNameserver: "ns.liquidweb.com", }, { ID: 12, - Name: "money.banana.example", + Name: "money.banana.com", Active: 1, DelegationStatus: "UNKNOWN", - PrimaryNameserver: "ns.example.org", + PrimaryNameserver: "ns.liquidweb.com", }, { ID: 13, - Name: "money.stand.banana.example", + Name: "money.stand.banana.com", Active: 1, DelegationStatus: "UNKNOWN", - PrimaryNameserver: "ns.example.org", + PrimaryNameserver: "ns.liquidweb.com", }, { ID: 14, - Name: "stand.banana.example", + Name: "stand.banana.com", Active: 1, DelegationStatus: "UNKNOWN", - PrimaryNameserver: "ns.example.org", + PrimaryNameserver: "ns.liquidweb.com", }, }, }, } mockAPIServerZones := make(map[string]int) - for _, page := range mockZones { for _, zone := range page.Items { mockAPIServerZones[zone.Name] = int(zone.ID) } } - return mockZones, mockAPIServerZones } diff --git a/providers/dns/loopia/internal/client.go b/providers/dns/loopia/internal/client.go index 0e9513024..d521ffeec 100644 --- a/providers/dns/loopia/internal/client.go +++ b/providers/dns/loopia/internal/client.go @@ -37,7 +37,7 @@ func NewClient(apiUser, apiPassword string) *Client { } // AddTXTRecord adds a TXT record. -func (c *Client) AddTXTRecord(ctx context.Context, domain, subdomain string, ttl int, value string) error { +func (c *Client) AddTXTRecord(ctx context.Context, domain string, subdomain string, ttl int, value string) error { call := &methodCall{ MethodName: "addZoneRecord", Params: []param{ @@ -67,7 +67,7 @@ func (c *Client) AddTXTRecord(ctx context.Context, domain, subdomain string, ttl } // RemoveTXTRecord removes a TXT record. -func (c *Client) RemoveTXTRecord(ctx context.Context, domain, subdomain string, recordID int) error { +func (c *Client) RemoveTXTRecord(ctx context.Context, domain string, subdomain string, recordID int) error { call := &methodCall{ MethodName: "removeZoneRecord", Params: []param{ @@ -89,7 +89,7 @@ func (c *Client) RemoveTXTRecord(ctx context.Context, domain, subdomain string, } // GetTXTRecords gets TXT records. -func (c *Client) GetTXTRecords(ctx context.Context, domain, subdomain string) ([]RecordObj, error) { +func (c *Client) GetTXTRecords(ctx context.Context, domain string, subdomain string) ([]RecordObj, error) { call := &methodCall{ MethodName: "getZoneRecords", Params: []param{ diff --git a/providers/dns/loopia/internal/client_test.go b/providers/dns/loopia/internal/client_test.go index fed7d94f1..4fe2e1fd0 100644 --- a/providers/dns/loopia/internal/client_test.go +++ b/providers/dns/loopia/internal/client_test.go @@ -1,80 +1,65 @@ package internal import ( + "context" "encoding/xml" + "fmt" + "io" "net/http" "net/http/httptest" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder(password string) *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("apiuser", password) - client.HTTPClient = server.Client() - client.BaseURL = server.URL + "/" - - return client, nil - }, - servermock.CheckHeader().WithContentType("text/xml"), - ) -} - func TestClient_AddZoneRecord(t *testing.T) { + serverResponses := map[string]string{ + addZoneRecordGoodAuth: responseOk, + addZoneRecordBadAuth: responseAuthError, + addZoneRecordNonValidDomain: responseUnknownError, + addZoneRecordEmptyResponse: "", + } + + serverURL := createFakeServer(t, serverResponses) + testCases := []struct { desc string password string domain string - request string - response string err string }{ { desc: "auth ok", password: "goodpassword", domain: exampleDomain, - request: addZoneRecordGoodAuth, - response: responseOk, }, { desc: "auth error", password: "badpassword", domain: exampleDomain, - request: addZoneRecordBadAuth, - response: responseAuthError, err: "authentication error", }, { desc: "unknown error", password: "goodpassword", domain: "badexample.com", - request: addZoneRecordNonValidDomain, - response: responseUnknownError, err: `unknown error: "UNKNOWN_ERROR"`, }, { desc: "empty response", password: "goodpassword", domain: "empty.com", - request: addZoneRecordEmptyResponse, - response: "", err: "unmarshal error: EOF", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - client := mockBuilder(test.password). - Route("POST /", - servermock.RawStringResponse(test.response), - servermock.CheckRequestBody(test.request)). - Build(t) + client := NewClient("apiuser", test.password) + client.BaseURL = serverURL + "/" - err := client.AddTXTRecord(t.Context(), test.domain, exampleSubDomain, 123, "TXTrecord") + err := client.AddTXTRecord(context.Background(), test.domain, exampleSubDomain, 123, "TXTrecord") if test.err == "" { require.NoError(t, err) } else { @@ -86,56 +71,52 @@ func TestClient_AddZoneRecord(t *testing.T) { } func TestClient_RemoveSubdomain(t *testing.T) { + serverResponses := map[string]string{ + removeSubdomainGoodAuth: responseOk, + removeSubdomainBadAuth: responseAuthError, + removeSubdomainNonValidDomain: responseUnknownError, + removeSubdomainEmptyResponse: "", + } + + serverURL := createFakeServer(t, serverResponses) + testCases := []struct { desc string password string domain string - request string - response string err string }{ { desc: "auth ok", password: "goodpassword", domain: exampleDomain, - request: removeSubdomainGoodAuth, - response: responseOk, }, { desc: "auth error", password: "badpassword", domain: exampleDomain, - request: removeSubdomainBadAuth, - response: responseAuthError, err: "authentication error", }, { desc: "unknown error", password: "goodpassword", domain: "badexample.com", - request: removeSubdomainNonValidDomain, - response: responseUnknownError, err: `unknown error: "UNKNOWN_ERROR"`, }, { desc: "empty response", password: "goodpassword", domain: "empty.com", - request: removeSubdomainEmptyResponse, - response: "", err: "unmarshal error: EOF", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - client := mockBuilder(test.password). - Route("POST /", - servermock.RawStringResponse(test.response), - servermock.CheckRequestBody(test.request)). - Build(t) + client := NewClient("apiuser", test.password) + client.BaseURL = serverURL + "/" - err := client.RemoveSubdomain(t.Context(), test.domain, exampleSubDomain) + err := client.RemoveSubdomain(context.Background(), test.domain, exampleSubDomain) if test.err == "" { require.NoError(t, err) } else { @@ -147,56 +128,52 @@ func TestClient_RemoveSubdomain(t *testing.T) { } func TestClient_RemoveZoneRecord(t *testing.T) { + serverResponses := map[string]string{ + removeRecordGoodAuth: responseOk, + removeRecordBadAuth: responseAuthError, + removeRecordNonValidDomain: responseUnknownError, + removeRecordEmptyResponse: "", + } + + serverURL := createFakeServer(t, serverResponses) + testCases := []struct { desc string password string domain string - request string - response string err string }{ { desc: "auth ok", password: "goodpassword", domain: exampleDomain, - request: removeRecordGoodAuth, - response: responseOk, }, { desc: "auth error", password: "badpassword", domain: exampleDomain, - request: removeRecordBadAuth, - response: responseAuthError, err: "authentication error", }, { desc: "uknown error", password: "goodpassword", domain: "badexample.com", - request: removeRecordNonValidDomain, - response: responseUnknownError, err: `unknown error: "UNKNOWN_ERROR"`, }, { desc: "empty response", password: "goodpassword", domain: "empty.com", - request: removeRecordEmptyResponse, - response: "", err: "unmarshal error: EOF", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - client := mockBuilder(test.password). - Route("POST /", - servermock.RawStringResponse(test.response), - servermock.CheckRequestBody(test.request)). - Build(t) + client := NewClient("apiuser", test.password) + client.BaseURL = serverURL + "/" - err := client.RemoveTXTRecord(t.Context(), test.domain, exampleSubDomain, 12345678) + err := client.RemoveTXTRecord(context.Background(), test.domain, exampleSubDomain, 12345678) if test.err == "" { require.NoError(t, err) } else { @@ -208,13 +185,16 @@ func TestClient_RemoveZoneRecord(t *testing.T) { } func TestClient_GetZoneRecord(t *testing.T) { - client := mockBuilder("goodpassword"). - Route("POST /", - servermock.RawStringResponse(getZoneRecordsResponse), - servermock.CheckRequestBody(getZoneRecords)). - Build(t) + serverResponses := map[string]string{ + getZoneRecords: getZoneRecordsResponse, + } - recordObjs, err := client.GetTXTRecords(t.Context(), exampleDomain, exampleSubDomain) + serverURL := createFakeServer(t, serverResponses) + + client := NewClient("apiuser", "goodpassword") + client.BaseURL = serverURL + "/" + + recordObjs, err := client.GetTXTRecords(context.Background(), exampleDomain, exampleSubDomain) require.NoError(t, err) expected := []RecordObj{ @@ -226,15 +206,27 @@ func TestClient_GetZoneRecord(t *testing.T) { RecordID: 12345678, }, } - assert.Equal(t, expected, recordObjs) + assert.EqualValues(t, expected, recordObjs) } func TestClient_rpcCall_404(t *testing.T) { - client := mockBuilder("apipassword"). - Route("POST /", - servermock.RawStringResponse(""). - WithStatusCode(http.StatusNotFound)). - Build(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusNotFound) + + _, err = fmt.Fprint(w, "") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + })) + + t.Cleanup(server.Close) call := &methodCall{ MethodName: "dummyMethod", @@ -243,15 +235,29 @@ func TestClient_rpcCall_404(t *testing.T) { }, } - err := client.rpcCall(t.Context(), call, &responseString{}) + client := NewClient("apiuser", "apipassword") + client.BaseURL = server.URL + "/" + + err := client.rpcCall(context.Background(), call, &responseString{}) require.EqualError(t, err, "unexpected status code: [status code: 404] body: ") } func TestClient_rpcCall_RPCError(t *testing.T) { - client := mockBuilder("apipassword"). - Route("POST /", - servermock.RawStringResponse(responseRPCError)). - Build(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + _, err = fmt.Fprint(w, responseRPCError) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + })) + + t.Cleanup(server.Close) call := &methodCall{ MethodName: "getDomains", @@ -260,7 +266,10 @@ func TestClient_rpcCall_RPCError(t *testing.T) { }, } - err := client.rpcCall(t.Context(), call, &responseString{}) + client := NewClient("apiuser", "apipassword") + client.BaseURL = server.URL + "/" + + err := client.rpcCall(context.Background(), call, &responseString{}) require.EqualError(t, err, "RPC Error: (201) Method signature error: 42") } @@ -292,3 +301,37 @@ func TestUnmarshallFaultyRecordObject(t *testing.T) { }) } } + +func createFakeServer(t *testing.T, serverResponses map[string]string) string { + t.Helper() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Content-Type") != "text/xml" { + http.Error(w, fmt.Sprintf("invalid content type: %s", r.Header.Get("Content-Type")), http.StatusBadRequest) + return + } + + req, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + resp, ok := serverResponses[string(req)] + if !ok { + http.Error(w, "no response for request", http.StatusBadRequest) + return + } + + _, err = fmt.Fprint(w, resp) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + server := httptest.NewServer(handler) + t.Cleanup(server.Close) + + return server.URL +} diff --git a/providers/dns/loopia/internal/types.go b/providers/dns/loopia/internal/types.go index c3425c8b1..c286c01fd 100644 --- a/providers/dns/loopia/internal/types.go +++ b/providers/dns/loopia/internal/types.go @@ -66,7 +66,6 @@ type response interface { type responseString struct { responseFault - Value string `xml:"params>param>value>string"` } @@ -89,7 +88,6 @@ func (e RPCError) Error() string { type recordObjectsResponse struct { responseFault - XMLName xml.Name `xml:"methodResponse"` Params []RecordObj `xml:"params>param>value>array>data>value>struct"` } @@ -104,7 +102,6 @@ type RecordObj struct { func (r *RecordObj) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var name string - for { t, err := d.Token() if err != nil { @@ -147,7 +144,6 @@ func (r *RecordObj) decodeValueString(name string, d *xml.Decoder, start xml.Sta } s = strings.TrimSpace(s) - switch name { case "type": r.Type = s diff --git a/providers/dns/loopia/loopia.go b/providers/dns/loopia/loopia.go index be3416ddf..582a247fa 100644 --- a/providers/dns/loopia/loopia.go +++ b/providers/dns/loopia/loopia.go @@ -9,13 +9,13 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/loopia/internal" ) +const minTTL = 300 + // Environment variables names. const ( envNamespace = "LOOPIA_" @@ -30,14 +30,10 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const minTTL = 300 - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - type dnsClient interface { - AddTXTRecord(ctx context.Context, domain, subdomain string, ttl int, value string) error - RemoveTXTRecord(ctx context.Context, domain, subdomain string, recordID int) error - GetTXTRecords(ctx context.Context, domain, subdomain string) ([]internal.RecordObj, error) + AddTXTRecord(ctx context.Context, domain string, subdomain string, ttl int, value string) error + RemoveTXTRecord(ctx context.Context, domain string, subdomain string, recordID int) error + GetTXTRecords(ctx context.Context, domain string, subdomain string) ([]internal.RecordObj, error) RemoveSubdomain(ctx context.Context, domain, subdomain string) error } @@ -57,9 +53,9 @@ func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 40*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 60*time.Second), HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, time.Minute), + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 60*time.Second), }, } } @@ -114,8 +110,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - if config.BaseURL != "" { client.BaseURL = config.BaseURL } diff --git a/providers/dns/loopia/loopia.toml b/providers/dns/loopia/loopia.toml index a201852c9..e0a75effd 100644 --- a/providers/dns/loopia/loopia.toml +++ b/providers/dns/loopia/loopia.toml @@ -7,7 +7,7 @@ Since = "v4.2.0" Example = ''' LOOPIA_API_USER=xxxxxxxx \ LOOPIA_API_PASSWORD=yyyyyyyy \ -lego --dns loopia -d '*.example.com' -d example.com run +lego --email my@email.com --dns loopia --domains my.domain.com run ''' Additional = ''' @@ -29,10 +29,10 @@ It needs to have the following permissions: LOOPIA_API_PASSWORD = "API password" [Configuration.Additional] LOOPIA_API_URL = "API endpoint. Ex: https://api.loopia.se/RPCSERV or https://api.loopia.rs/RPCSERV" - LOOPIA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2400)" - LOOPIA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - LOOPIA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - LOOPIA_HTTP_TIMEOUT = "API request timeout in seconds (Default: 60)" + LOOPIA_POLLING_INTERVAL = "Time between DNS propagation check" + LOOPIA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + LOOPIA_TTL = "The TTL of the TXT record used for the DNS challenge" + LOOPIA_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.loopia.com/api" diff --git a/providers/dns/loopia/loopia_mock_test.go b/providers/dns/loopia/loopia_mock_test.go index fb0bcaa2b..93f26af06 100644 --- a/providers/dns/loopia/loopia_mock_test.go +++ b/providers/dns/loopia/loopia_mock_test.go @@ -215,17 +215,17 @@ type mockedClient struct { mock.Mock } -func (c *mockedClient) RemoveTXTRecord(ctx context.Context, domain, subdomain string, recordID int) error { +func (c *mockedClient) RemoveTXTRecord(ctx context.Context, domain string, subdomain string, recordID int) error { args := c.Called(domain, subdomain, recordID) return args.Error(0) } -func (c *mockedClient) AddTXTRecord(ctx context.Context, domain, subdomain string, ttl int, value string) error { +func (c *mockedClient) AddTXTRecord(ctx context.Context, domain string, subdomain string, ttl int, value string) error { args := c.Called(domain, subdomain, ttl, value) return args.Error(0) } -func (c *mockedClient) GetTXTRecords(ctx context.Context, domain, subdomain string) ([]internal.RecordObj, error) { +func (c *mockedClient) GetTXTRecords(ctx context.Context, domain string, subdomain string) ([]internal.RecordObj, error) { args := c.Called(domain, subdomain) return args.Get(0).([]internal.RecordObj), args.Error(1) } diff --git a/providers/dns/loopia/loopia_test.go b/providers/dns/loopia/loopia_test.go index b3163fc77..e397c9639 100644 --- a/providers/dns/loopia/loopia_test.go +++ b/providers/dns/loopia/loopia_test.go @@ -103,7 +103,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -193,7 +192,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -207,7 +205,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/luadns/internal/client.go b/providers/dns/luadns/internal/client.go index 5ce9cca86..8e46418f2 100644 --- a/providers/dns/luadns/internal/client.go +++ b/providers/dns/luadns/internal/client.go @@ -49,7 +49,6 @@ func (c *Client) ListZones(ctx context.Context) ([]DNSZone, error) { } var zones []DNSZone - err = c.do(req, &zones) if err != nil { return nil, fmt.Errorf("could not list zones: %w", err) @@ -69,7 +68,6 @@ func (c *Client) CreateRecord(ctx context.Context, zone DNSZone, newRecord DNSRe } var record *DNSRecord - err = c.do(req, &record) if err != nil { return nil, fmt.Errorf("could not create record %#v: %w", record, err) @@ -155,7 +153,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) var errResp errorResponse - err := json.Unmarshal(raw, &errResp) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/luadns/internal/client_test.go b/providers/dns/luadns/internal/client_test.go index 0a3a79e6c..1fd3efd74 100644 --- a/providers/dns/luadns/internal/client_test.go +++ b/providers/dns/luadns/internal/client_test.go @@ -1,34 +1,63 @@ package internal import ( + "context" + "fmt" + "io" + "net/http" "net/http/httptest" "net/url" + "os" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder(apiToken string) *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("me", apiToken) - client.baseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() +func setupTest(t *testing.T, apiToken string) (*Client, *http.ServeMux) { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithBasicAuth("me", apiToken)) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient("me", apiToken) + client.baseURL, _ = url.Parse(server.URL) + client.HTTPClient = server.Client() + + return client, mux } func TestClient_ListZones(t *testing.T) { - client := mockBuilder("secretA"). - Route("GET /v1/zones", servermock.ResponseFromFixture("list_zones.json")). - Build(t) + client, mux := setupTest(t, "secretA") - zones, err := client.ListZones(t.Context()) + mux.HandleFunc("/v1/zones", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, fmt.Sprintf("invalid method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + auth := req.Header.Get("Authorization") + if auth != "Basic bWU6c2VjcmV0QQ==" { + http.Error(rw, fmt.Sprintf("invalid authentication: %s", auth), http.StatusUnauthorized) + return + } + + file, err := os.Open("./fixtures/list_zones.json") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + zones, err := client.ListZones(context.Background()) require.NoError(t, err) expected := []DNSZone{ @@ -60,11 +89,33 @@ func TestClient_ListZones(t *testing.T) { } func TestClient_CreateRecord(t *testing.T) { - client := mockBuilder("secretB"). - Route("POST /v1/zones/1/records", - servermock.ResponseFromFixture("create_record.json"), - servermock.CheckRequestJSONBody(`{"name":"example.com.","type":"MX","content":"10 mail.example.com.","ttl":300}`)). - Build(t) + client, mux := setupTest(t, "secretB") + + mux.HandleFunc("/v1/zones/1/records", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("invalid method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + auth := req.Header.Get("Authorization") + if auth != "Basic bWU6c2VjcmV0Qg==" { + http.Error(rw, fmt.Sprintf("invalid authentication: %s", auth), http.StatusUnauthorized) + return + } + + file, err := os.Open("./fixtures/create_record.json") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) zone := DNSZone{ID: 1} @@ -75,7 +126,7 @@ func TestClient_CreateRecord(t *testing.T) { TTL: 300, } - newRecord, err := client.CreateRecord(t.Context(), zone, record) + newRecord, err := client.CreateRecord(context.Background(), zone, record) require.NoError(t, err) expected := &DNSRecord{ @@ -91,11 +142,33 @@ func TestClient_CreateRecord(t *testing.T) { } func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder("secretC"). - Route("DELETE /v1/zones/1/records/2", - servermock.ResponseFromFixture("delete_record.json"), - servermock.CheckRequestJSONBody(`{"id":2,"name":"example.com.","type":"MX","content":"10 mail.example.com.","ttl":300,"zone_id":1}`)). - Build(t) + client, mux := setupTest(t, "secretC") + + mux.HandleFunc("/v1/zones/1/records/2", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodDelete { + http.Error(rw, fmt.Sprintf("invalid method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + auth := req.Header.Get("Authorization") + if auth != "Basic bWU6c2VjcmV0Qw==" { + http.Error(rw, fmt.Sprintf("invalid authentication: %s", auth), http.StatusUnauthorized) + return + } + + file, err := os.Open("./fixtures/delete_record.json") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) record := &DNSRecord{ ID: 2, @@ -106,6 +179,6 @@ func TestClient_DeleteRecord(t *testing.T) { ZoneID: 1, } - err := client.DeleteRecord(t.Context(), record) + err := client.DeleteRecord(context.Background(), record) require.NoError(t, err) } diff --git a/providers/dns/luadns/luadns.go b/providers/dns/luadns/luadns.go index 68b9c66b8..97261e157 100644 --- a/providers/dns/luadns/luadns.go +++ b/providers/dns/luadns/luadns.go @@ -10,13 +10,13 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/luadns/internal" ) +const minTTL = 300 + // Environment variables names. const ( envNamespace = "LUADNS_" @@ -30,10 +30,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const minTTL = 300 - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIUsername string @@ -49,7 +45,7 @@ func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, @@ -101,12 +97,11 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ - config: config, - client: client, - records: make(map[string]*internal.DNSRecord), + config: config, + client: client, + recordsMu: sync.Mutex{}, + records: make(map[string]*internal.DNSRecord), }, nil } diff --git a/providers/dns/luadns/luadns.toml b/providers/dns/luadns/luadns.toml index e56fac0b6..60c11c815 100644 --- a/providers/dns/luadns/luadns.toml +++ b/providers/dns/luadns/luadns.toml @@ -7,7 +7,7 @@ Since = "v3.7.0" Example = ''' LUADNS_API_USERNAME=youremail \ LUADNS_API_TOKEN=xxxxxxxx \ -lego --dns luadns -d '*.example.com' -d example.com run +lego --email you@example.com --dns luadns --domains my.example.org run ''' [Configuration] @@ -15,10 +15,10 @@ lego --dns luadns -d '*.example.com' -d example.com run LUADNS_API_USERNAME = "Username (your email)" LUADNS_API_TOKEN = "API token" [Configuration.Additional] - LUADNS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - LUADNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - LUADNS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - LUADNS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + LUADNS_POLLING_INTERVAL = "Time between DNS propagation check" + LUADNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + LUADNS_TTL = "The TTL of the TXT record used for the DNS challenge" + LUADNS_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://luadns.com/api.html" diff --git a/providers/dns/luadns/luadns_test.go b/providers/dns/luadns/luadns_test.go index a1aa36872..ea4d06ae1 100644 --- a/providers/dns/luadns/luadns_test.go +++ b/providers/dns/luadns/luadns_test.go @@ -58,7 +58,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -200,7 +199,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -214,7 +212,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/mailinabox/mailinabox.go b/providers/dns/mailinabox/mailinabox.go index cf6202a92..4d366379e 100644 --- a/providers/dns/mailinabox/mailinabox.go +++ b/providers/dns/mailinabox/mailinabox.go @@ -5,13 +5,10 @@ import ( "context" "errors" "fmt" - "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/nrdcg/mailinabox" ) @@ -25,11 +22,8 @@ const ( EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Email string @@ -37,7 +31,6 @@ type Config struct { BaseURL string PropagationTimeout time.Duration PollingInterval time.Duration - HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. @@ -45,9 +38,6 @@ func NewDefaultConfig() *Config { return &Config{ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 4*time.Second), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, } } @@ -88,13 +78,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("mailinabox: missing base URL") } - if config.HTTPClient == nil { - config.HTTPClient = &http.Client{Timeout: 30 * time.Second} - } - - config.HTTPClient = clientdebug.Wrap(config.HTTPClient) - - client, err := mailinabox.New(config.BaseURL, config.Email, config.Password, mailinabox.WithHTTPClient(config.HTTPClient)) + client, err := mailinabox.New(config.BaseURL, config.Email, config.Password) if err != nil { return nil, fmt.Errorf("mailinabox: %w", err) } diff --git a/providers/dns/mailinabox/mailinabox.toml b/providers/dns/mailinabox/mailinabox.toml index 74d8aabbc..fdfef081b 100644 --- a/providers/dns/mailinabox/mailinabox.toml +++ b/providers/dns/mailinabox/mailinabox.toml @@ -8,7 +8,7 @@ Example = ''' MAILINABOX_EMAIL=user@example.com \ MAILINABOX_PASSWORD=yyyy \ MAILINABOX_BASE_URL=https://box.example.com \ -lego --dns mailinabox -d '*.example.com' -d example.com run +lego --email you@example.com --dns mailinabox --domains my.example.org run ''' [Configuration] @@ -17,9 +17,8 @@ lego --dns mailinabox -d '*.example.com' -d example.com run MAILINABOX_PASSWORD = "User password" MAILINABOX_BASE_URL = "Base API URL (ex: https://box.example.com)" [Configuration.Additional] - MAILINABOX_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 4)" - MAILINABOX_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - MAILINABOX_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + MAILINABOX_POLLING_INTERVAL = "Time between DNS propagation check" + MAILINABOX_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" [Links] API = "https://mailinabox.email/api-docs.html" diff --git a/providers/dns/mailinabox/mailinabox_test.go b/providers/dns/mailinabox/mailinabox_test.go index 11143a11f..1b95c220d 100644 --- a/providers/dns/mailinabox/mailinabox_test.go +++ b/providers/dns/mailinabox/mailinabox_test.go @@ -59,7 +59,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -137,7 +136,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -151,7 +149,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/manageengine/internal/client.go b/providers/dns/manageengine/internal/client.go deleted file mode 100644 index b5a7dbae7..000000000 --- a/providers/dns/manageengine/internal/client.go +++ /dev/null @@ -1,199 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "strings" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" -) - -const defaultBaseURL = "https://clouddns.manageengine.com/v1" - -// Client the ManageEngine CloudDNS API client. -type Client struct { - baseURL *url.URL - httpClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(hc *http.Client) *Client { - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - baseURL: baseURL, - httpClient: hc, - } -} - -// GetAllZones gets all zones. -// https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation#GET_All -func (c *Client) GetAllZones(ctx context.Context) ([]Zone, error) { - endpoint := c.baseURL.JoinPath("dns", "domain") - - req, err := newRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var results []Zone - - err = c.do(req, &results) - if err != nil { - return nil, err - } - - return results, nil -} - -// GetAllZoneRecords gets all "zone records" for a zone. -// https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation#GET_All_9 -func (c *Client) GetAllZoneRecords(ctx context.Context, zoneID int) ([]ZoneRecord, error) { - endpoint := c.baseURL.JoinPath("dns", "domain", strconv.Itoa(zoneID), "records", "SPF_TXT") - - req, err := newRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var results []ZoneRecord - - err = c.do(req, &results) - if err != nil { - return nil, err - } - - return results, nil -} - -// DeleteZoneRecord deletes a "zone record". -// https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation#DEL_Delete_10 -func (c *Client) DeleteZoneRecord(ctx context.Context, zoneID, domainID int) error { - endpoint := c.baseURL.JoinPath("dns", "domain", strconv.Itoa(zoneID), "records", "SPF_TXT", strconv.Itoa(domainID)) - - req, err := newRequest(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return err - } - - var results APIResponse - - return c.do(req, &results) -} - -// CreateZoneRecord creates a "zone record". -// https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation#POST_Create_10 -func (c *Client) CreateZoneRecord(ctx context.Context, zoneID int, record ZoneRecord) error { - endpoint := c.baseURL.JoinPath("dns", "domain", strconv.Itoa(zoneID), "records", "SPF_TXT", "/") - - req, err := newRequest(ctx, http.MethodPost, endpoint, []ZoneRecord{record}) - if err != nil { - return err - } - - var results APIResponse - - return c.do(req, &results) -} - -// UpdateZoneRecord update an existing "zone record". -// https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation#PUT_Update_10 -func (c *Client) UpdateZoneRecord(ctx context.Context, record ZoneRecord) error { - if record.SpfTxtDomainID == 0 { - return errors.New("SpfTxtDomainID is empty") - } - - if record.ZoneID == 0 { - return errors.New("ZoneID is empty") - } - - endpoint := c.baseURL.JoinPath("dns", "domain", strconv.Itoa(record.ZoneID), "records", "SPF_TXT", strconv.Itoa(record.SpfTxtDomainID), "/") - - req, err := newRequest(ctx, http.MethodPut, endpoint, []ZoneRecord{record}) - if err != nil { - return err - } - - var results APIResponse - - return c.do(req, &results) -} - -func (c *Client) do(req *http.Request, result any) error { - resp, err := c.httpClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - var body io.Reader = http.NoBody - - if payload != nil { - buf := new(bytes.Buffer) - - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - - values := url.Values{} - values.Set("config", buf.String()) - body = strings.NewReader(values.Encode()) - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIError - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return fmt.Errorf("[status code: %d] %w", resp.StatusCode, &errAPI) -} diff --git a/providers/dns/manageengine/internal/client_test.go b/providers/dns/manageengine/internal/client_test.go deleted file mode 100644 index 25d1730f6..000000000 --- a/providers/dns/manageengine/internal/client_test.go +++ /dev/null @@ -1,302 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient(server.Client()) - - client.baseURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckHeader(). - WithAccept("application/json")) -} - -func TestClient_GetAllZones(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/domain", servermock.ResponseFromFixture("zone_domains_all.json")). - Build(t) - - groups, err := client.GetAllZones(t.Context()) - require.NoError(t, err) - - expected := []Zone{ - { - ZoneID: 1, - ZoneName: "test.com.", - ZoneTTL: 500, - ZoneTargeting: true, - Refresh: 43200, - Retry: 3600, - Expiry: 1209600, - Minimum: 180, - Org: 2, - NsID: 1, - Serial: 2022042206, - Nss: []string{"ns11.zns-53.com.", "ns21.zns-53.net.", "ns31.zns-53.com.", "ns41.zns-53.net."}, - }, - { - ZoneID: 2, - ZoneName: "yourdomain.com.", - ZoneTTL: 1000, - Refresh: 43200, - Retry: 3600, - Expiry: 1209600, - Minimum: 180, - Org: 2, - Vanity: true, - NsID: 1, - Serial: 2022040608, - Nss: []string{"ns11.yourdomain.com.", "ns21.yourdomain.net.", "ns31.yourdomain.com.", "ns41.yourdomain.net."}, - }, - { - ZoneID: 20, - ZoneName: "hello45.com.", - ZoneTTL: 3000, - Refresh: 43200, - Retry: 3600, - Expiry: 1209600, - Minimum: 180, - Org: 2, - NsID: 1, - Serial: 2022040711, - Nss: []string{"ns11.zns-53.com.", "ns21.zns-53.net.", "ns31.zns-53.com.", "ns41.zns-53.net."}, - }, - { - ZoneID: 22, - ZoneName: "zohoaccl.com.", - ZoneTTL: 300, - ZoneTargeting: true, - Refresh: 43200, - Retry: 3600, - Expiry: 1209600, - Minimum: 180, - Org: 2, - NsID: 1, - Serial: 2022042206, - Nss: []string{"ns11.zns-53.com.", "ns21.zns-53.net.", "ns31.zns-53.com.", "ns41.zns-53.net."}, - }, - { - ZoneID: 23, - ZoneName: "zohocal.com.", - ZoneTTL: 300, - ZoneTargeting: true, - Refresh: 43200, - Retry: 3600, - Expiry: 1209600, - Minimum: 180, - Org: 2, - NsID: 1, - Serial: 2022041310, - Nss: []string{"ns11.zns-53.com.", "ns21.zns-53.net.", "ns31.zns-53.com.", "ns41.zns-53.net."}, - }, - } - - assert.Equal(t, expected, groups) -} - -func TestClient_GetAllZones_error(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/domain", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) - - _, err := client.GetAllZones(t.Context()) - require.Error(t, err) - - require.EqualError(t, err, "[status code: 401] Authentication credentials were not provided.") -} - -func TestClient_GetAllZoneRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/domain/4/records/SPF_TXT", servermock.ResponseFromFixture("zone_records_all.json")). - Build(t) - - groups, err := client.GetAllZoneRecords(t.Context(), 4) - require.NoError(t, err) - - expected := []ZoneRecord{ - { - ZoneID: 4, - SpfTxtDomainID: 6, - DomainName: "spftest.example.com.", - DomainTTL: 300, - DomainLocationID: 1, - RecordType: "SPF", - Records: []Record{{ - ID: 1, - Values: []string{"necwcltpwxbz-noelget3jush-vop2xxvapot3eyq_0"}, - DomainID: 6, - }}, - }, - { - ZoneID: 4, - SpfTxtDomainID: 13, - DomainName: "txt.example.com.", - DomainTTL: 300, - DomainLocationID: 1, - RecordType: "TXT", - Records: []Record{{ - ID: 1, - Values: []string{"v=spf1include:transmail.netinclude:example.com~all", "c-68e3oc4trm8w7piplscg7vgojmtkjrnrabr4king8"}, - DomainID: 13, - }}, - }, - } - - assert.Equal(t, expected, groups) -} - -func TestClient_GetAllZoneRecords_error(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/domain/4/records/SPF_TXT", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) - - _, err := client.GetAllZoneRecords(t.Context(), 4) - require.Error(t, err) - - require.EqualError(t, err, "[status code: 401] Authentication credentials were not provided.") -} - -func TestClient_DeleteZoneRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /dns/domain/4/records/SPF_TXT/6", servermock.ResponseFromFixture("zone_record_delete.json")). - Build(t) - - err := client.DeleteZoneRecord(t.Context(), 4, 6) - require.NoError(t, err) -} - -func TestClient_DeleteZoneRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /dns/domain/4/records/SPF_TXT/6", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) - - err := client.DeleteZoneRecord(t.Context(), 4, 6) - require.Error(t, err) - - require.EqualError(t, err, "[status code: 401] Authentication credentials were not provided.") -} - -func TestClient_CreateZoneRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /dns/domain/4/records/SPF_TXT/", - servermock.ResponseFromFixture("zone_record_create.json"), - servermock.CheckHeader(). - WithContentTypeFromURLEncoded(), - servermock.CheckForm().Strict(). - With("config", `[{"zone_id":1,"spf_txt_domain_id":2,"domain_name":"example.com","domain_ttl":120,"domain_location_id":3,"record_type":"TXT","records":[{"record_id":123,"value":["value1"],"domain_id":1}]}] -`)). - Build(t) - - record := ZoneRecord{ - ZoneID: 1, - SpfTxtDomainID: 2, - DomainName: "example.com", - DomainTTL: 120, - DomainLocationID: 3, - RecordType: "TXT", - Records: []Record{ - { - ID: 123, - Values: []string{"value1"}, - Disabled: false, - DomainID: 1, - }, - }, - } - - err := client.CreateZoneRecord(t.Context(), 4, record) - require.NoError(t, err) -} - -func TestClient_CreateZoneRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /dns/domain/4/records/SPF_TXT/", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized), - servermock.CheckHeader(). - WithContentTypeFromURLEncoded()). - Build(t) - - record := ZoneRecord{} - - err := client.CreateZoneRecord(t.Context(), 4, record) - require.Error(t, err) - - require.EqualError(t, err, "[status code: 401] Authentication credentials were not provided.") -} - -func TestClient_CreateZoneRecord_error_bad_request(t *testing.T) { - client := mockBuilder(). - Route("POST /dns/domain/4/records/SPF_TXT/", - servermock.ResponseFromFixture("error_bad_request.json"). - WithStatusCode(http.StatusBadRequest), - servermock.CheckHeader(). - WithContentTypeFromURLEncoded()). - Build(t) - - record := ZoneRecord{} - - err := client.CreateZoneRecord(t.Context(), 4, record) - require.Error(t, err) - - require.EqualError(t, err, "[status code: 400] Invalid record format, Record should be in list.") -} - -func TestClient_UpdateZoneRecord(t *testing.T) { - client := mockBuilder(). - Route("PUT /dns/domain/4/records/SPF_TXT/6/", - servermock.ResponseFromFixture("zone_record_update.json"), - servermock.CheckHeader(). - WithContentTypeFromURLEncoded(), - servermock.CheckForm().Strict(). - With("config", `[{"zone_id":4,"spf_txt_domain_id":6,"records":null}] -`)). - Build(t) - - record := ZoneRecord{ - SpfTxtDomainID: 6, - ZoneID: 4, - } - - err := client.UpdateZoneRecord(t.Context(), record) - require.NoError(t, err) -} - -func TestClient_UpdateZoneRecord_error(t *testing.T) { - client := mockBuilder(). - Route("PUT /dns/domain/4/records/SPF_TXT/6/", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized), - servermock.CheckHeader(). - WithContentTypeFromURLEncoded()). - Build(t) - - record := ZoneRecord{ - SpfTxtDomainID: 6, - ZoneID: 4, - } - - err := client.UpdateZoneRecord(t.Context(), record) - require.Error(t, err) - - require.EqualError(t, err, "[status code: 401] Authentication credentials were not provided.") -} diff --git a/providers/dns/manageengine/internal/fixtures/error.json b/providers/dns/manageengine/internal/fixtures/error.json deleted file mode 100644 index 5cd198670..000000000 --- a/providers/dns/manageengine/internal/fixtures/error.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "detail": "Authentication credentials were not provided." -} diff --git a/providers/dns/manageengine/internal/fixtures/error_bad_request.json b/providers/dns/manageengine/internal/fixtures/error_bad_request.json deleted file mode 100644 index 944cef6c0..000000000 --- a/providers/dns/manageengine/internal/fixtures/error_bad_request.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "error": "Invalid record format, Record should be in list." -} diff --git a/providers/dns/manageengine/internal/fixtures/zone_domains_all.json b/providers/dns/manageengine/internal/fixtures/zone_domains_all.json deleted file mode 100644 index 3e37f52a7..000000000 --- a/providers/dns/manageengine/internal/fixtures/zone_domains_all.json +++ /dev/null @@ -1,146 +0,0 @@ -[ - { - "zone_id": 1, - "zone_name": "test.com.", - "zone_ttl": 500, - "zone_type": 0, - "zone_targeting": true, - "zone_logging": "{}", - "zone_contact": "mathes.zoho.com", - "refresh": 43200, - "retry": 3600, - "expiry": 1209600, - "minimum": 180, - "org": 2, - "any_query": false, - "dnssec": true, - "vanity": false, - "ns_id": 1, - "serial": 2022042206, - "ns": [ - "ns11.zns-53.com.", - "ns21.zns-53.net.", - "ns31.zns-53.com.", - "ns41.zns-53.net." - ], - "contact_group": [ - "test_contact1", - "test_contact2" - ], - "ds": [ - { - "record_id": 59, - "keyTag": 36938, - "algorithm": 13, - "digestType": 1, - "digest": "e9f03d176455d5d16f826b69f9ecb11f59be35e7", - "domain_id": 30 - }, - { - "record_id": 60, - "keyTag": 36938, - "algorithm": 13, - "digestType": 2, - "digest": "7ea640a8668eafd9d89a9b2e9994f5fcfb1dee0668d1e93ba556aa57ac047f96", - "domain_id": 30 - } - ] - }, - { - "zone_id": 2, - "zone_name": "yourdomain.com.", - "zone_ttl": 1000, - "zone_type": 0, - "zone_targeting": false, - "zone_logging": "{}", - "zone_contact": "contact.yourdomain.com", - "refresh": 43200, - "retry": 3600, - "expiry": 1209600, - "minimum": 180, - "org": 2, - "any_query": false, - "dnssec": false, - "vanity": true, - "vanity_grp": "yourdomain", - "ns_id": 1, - "serial": 2022040608, - "ns": [ - "ns11.yourdomain.com.", - "ns21.yourdomain.net.", - "ns31.yourdomain.com.", - "ns41.yourdomain.net." - ] - }, - { - "zone_id": 20, - "zone_name": "hello45.com.", - "zone_ttl": 3000, - "zone_targeting": false, - "zone_logging": "{}", - "zone_contact": "mathes.zoho.com", - "refresh": 43200, - "retry": 3600, - "expiry": 1209600, - "minimum": 180, - "org": 2, - "any_query": false, - "dnssec": false, - "ns_id": 1, - "serial": 2022040711, - "ns": [ - "ns11.zns-53.com.", - "ns21.zns-53.net.", - "ns31.zns-53.com.", - "ns41.zns-53.net." - ] - }, - { - "zone_id": 22, - "zone_name": "zohoaccl.com.", - "zone_ttl": 300, - "zone_type": 0, - "zone_targeting": true, - "zone_logging": "{}", - "zone_contact": "networkone.zohocorp.com", - "refresh": 43200, - "retry": 3600, - "expiry": 1209600, - "minimum": 180, - "org": 2, - "any_query": false, - "dnssec": false, - "ns_id": 1, - "serial": 2022042206, - "ns": [ - "ns11.zns-53.com.", - "ns21.zns-53.net.", - "ns31.zns-53.com.", - "ns41.zns-53.net." - ] - }, - { - "zone_id": 23, - "zone_name": "zohocal.com.", - "zone_ttl": 300, - "zone_type": 0, - "zone_targeting": true, - "zone_logging": "{}", - "zone_contact": "mathes.zoho.com", - "refresh": 43200, - "retry": 3600, - "expiry": 1209600, - "minimum": 180, - "org": 2, - "any_query": false, - "dnssec": false, - "ns_id": 1, - "serial": 2022041310, - "ns": [ - "ns11.zns-53.com.", - "ns21.zns-53.net.", - "ns31.zns-53.com.", - "ns41.zns-53.net." - ] - } -] diff --git a/providers/dns/manageengine/internal/fixtures/zone_record_create.json b/providers/dns/manageengine/internal/fixtures/zone_record_create.json deleted file mode 100644 index 3fd216f2d..000000000 --- a/providers/dns/manageengine/internal/fixtures/zone_record_create.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "message": "Record created successfully" -} diff --git a/providers/dns/manageengine/internal/fixtures/zone_record_delete.json b/providers/dns/manageengine/internal/fixtures/zone_record_delete.json deleted file mode 100644 index c657d84ea..000000000 --- a/providers/dns/manageengine/internal/fixtures/zone_record_delete.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "message": "Record deleted successfully" -} diff --git a/providers/dns/manageengine/internal/fixtures/zone_record_update.json b/providers/dns/manageengine/internal/fixtures/zone_record_update.json deleted file mode 100644 index 178c1fb0f..000000000 --- a/providers/dns/manageengine/internal/fixtures/zone_record_update.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "message": "Record updated successfully" -} diff --git a/providers/dns/manageengine/internal/fixtures/zone_records_all.json b/providers/dns/manageengine/internal/fixtures/zone_records_all.json deleted file mode 100644 index ae08a4c7e..000000000 --- a/providers/dns/manageengine/internal/fixtures/zone_records_all.json +++ /dev/null @@ -1,40 +0,0 @@ -[ - { - "spf_txt_domain_id": 6, - "zone_id": 4, - "domain_name": "spftest.example.com.", - "domain_ttl": 300, - "domain_location_id": 1, - "record_type": "SPF", - "records": [ - { - "record_id": 1, - "value": [ - "necwcltpwxbz-noelget3jush-vop2xxvapot3eyq_0" - ], - "disabled": false, - "domain_id": 6 - } - ] - }, - { - "spf_txt_domain_id": 13, - "zone_id": 4, - "domain_name": "txt.example.com.", - "domain_ttl": 300, - "domain_maxhost": 1, - "domain_location_id": 1, - "record_type": "TXT", - "records": [ - { - "record_id": 1, - "value": [ - "v=spf1include:transmail.netinclude:example.com~all", - "c-68e3oc4trm8w7piplscg7vgojmtkjrnrabr4king8" - ], - "disabled": false, - "domain_id": 13 - } - ] - } -] diff --git a/providers/dns/manageengine/internal/identity.go b/providers/dns/manageengine/internal/identity.go deleted file mode 100644 index ec28121e4..000000000 --- a/providers/dns/manageengine/internal/identity.go +++ /dev/null @@ -1,20 +0,0 @@ -package internal - -import ( - "context" - "net/http" - - "golang.org/x/oauth2/clientcredentials" -) - -const defaultAuthURL = "https://clouddns.manageengine.com/oauth2/token/" - -func CreateOAuthClient(ctx context.Context, clientID, clientSecret string) *http.Client { - config := &clientcredentials.Config{ - TokenURL: defaultAuthURL, - ClientID: clientID, - ClientSecret: clientSecret, - } - - return config.Client(ctx) -} diff --git a/providers/dns/manageengine/internal/types.go b/providers/dns/manageengine/internal/types.go deleted file mode 100644 index 7a039f67f..000000000 --- a/providers/dns/manageengine/internal/types.go +++ /dev/null @@ -1,63 +0,0 @@ -package internal - -import ( - "strings" -) - -type APIError struct { - Message string `json:"error"` - Detail string `json:"detail"` -} - -func (a *APIError) Error() string { - var msg []string - - if a.Message != "" { - msg = append(msg, a.Message) - } - - if a.Detail != "" { - msg = append(msg, a.Detail) - } - - return strings.Join(msg, " ") -} - -type APIResponse struct { - Message string `json:"message,omitempty"` -} - -type ZoneRecord struct { - ZoneID int `json:"zone_id,omitempty"` - SpfTxtDomainID int `json:"spf_txt_domain_id,omitempty"` - DomainName string `json:"domain_name,omitempty"` - DomainTTL int `json:"domain_ttl,omitempty"` - DomainLocationID int `json:"domain_location_id,omitempty"` - RecordType string `json:"record_type,omitempty"` - Records []Record `json:"records"` -} - -type Record struct { - ID int `json:"record_id,omitempty"` - Values []string `json:"value,omitempty"` - Disabled bool `json:"disabled,omitempty"` - DomainID int `json:"domain_id,omitempty"` -} - -type Zone struct { - ZoneID int `json:"zone_id"` - ZoneName string `json:"zone_name"` - ZoneTTL int `json:"zone_ttl"` - ZoneType int `json:"zone_type,omitempty"` - ZoneTargeting bool `json:"zone_targeting"` - Refresh int `json:"refresh"` - Retry int `json:"retry"` - Expiry int `json:"expiry"` - Minimum int `json:"minimum"` - Org int `json:"org"` - AnyQuery bool `json:"any_query"` - Vanity bool `json:"vanity,omitempty"` - NsID int `json:"ns_id"` - Serial int `json:"serial"` - Nss []string `json:"ns"` -} diff --git a/providers/dns/manageengine/manageengine.go b/providers/dns/manageengine/manageengine.go deleted file mode 100644 index 76b6644c0..000000000 --- a/providers/dns/manageengine/manageengine.go +++ /dev/null @@ -1,266 +0,0 @@ -// Package manageengine implements a DNS provider for solving the DNS-01 challenge using ManageEngine CloudDNS. -package manageengine - -import ( - "context" - "errors" - "fmt" - "slices" - "strings" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/manageengine/internal" -) - -// Environment variables names. -const ( - envNamespace = "MANAGEENGINE_" - - EnvClientID = envNamespace + "CLIENT_ID" - EnvClientSecret = envNamespace + "CLIENT_SECRET" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - ClientID string - ClientSecret string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for ManageEngine CloudDNS. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvClientID, EnvClientSecret) - if err != nil { - return nil, fmt.Errorf("manageengine: %w", err) - } - - config := NewDefaultConfig() - config.ClientID = values[EnvClientID] - config.ClientSecret = values[EnvClientSecret] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for ManageEngine CloudDNS. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("manageengine: the configuration of the DNS provider is nil") - } - - if config.ClientID == "" || config.ClientSecret == "" { - return nil, errors.New("manageengine: credentials missing") - } - - return &DNSProvider{ - config: config, - client: internal.NewClient( - clientdebug.Wrap( - internal.CreateOAuthClient(context.Background(), config.ClientID, config.ClientSecret), - ), - ), - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("manageengine: could not find zone for domain %q: %w", domain, err) - } - - zoneID, err := d.findZoneID(ctx, authZone) - if err != nil { - return fmt.Errorf("manageengine: find zone ID: %w", err) - } - - zoneRecord, err := d.findZoneRecord(ctx, zoneID, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("manageengine: find zone record: %w", err) - } - - // Update the existing zone record. - if zoneRecord != nil { - for _, record := range zoneRecord.Records { - if slices.Contains(record.Values, info.Value) { - continue - } - - zr := internal.ZoneRecord{ - ZoneID: zoneID, - SpfTxtDomainID: zoneRecord.SpfTxtDomainID, - DomainName: info.EffectiveFQDN, - DomainTTL: d.config.TTL, - RecordType: "TXT", - Records: []internal.Record{{ - Values: append(record.Values, info.Value), - DomainID: zoneRecord.SpfTxtDomainID, - }}, - } - - // Update the zone record. - err = d.client.UpdateZoneRecord(ctx, zr) - if err != nil { - return fmt.Errorf("manageengine: update zone record: %w", err) - } - - return nil - } - - return errors.New("manageengine: zone already contains the TXT record value") - } - - // Create a new zone record. - record := internal.ZoneRecord{ - ZoneID: zoneID, - DomainName: info.EffectiveFQDN, - DomainTTL: d.config.TTL, - RecordType: "TXT", - Records: []internal.Record{{ - Values: []string{info.Value}, - }}, - } - - err = d.client.CreateZoneRecord(ctx, zoneID, record) - if err != nil { - return fmt.Errorf("manageengine: create zone record: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("manageengine: could not find zone for domain %q: %w", domain, err) - } - - zoneID, err := d.findZoneID(ctx, authZone) - if err != nil { - return fmt.Errorf("manageengine: find zone ID: %w", err) - } - - zoneRecord, err := d.findZoneRecord(ctx, zoneID, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("manageengine: find zone record: %w", err) - } - - for _, record := range zoneRecord.Records { - if !slices.Contains(record.Values, info.Value) { - continue - } - - // Delete the zone record. - if len(record.Values) <= 1 { - err = d.client.DeleteZoneRecord(ctx, zoneID, zoneRecord.SpfTxtDomainID) - if err != nil { - return fmt.Errorf("manageengine: delete zone record: %w", err) - } - - return nil - } - - // Update the zone record. - var values []string - - for _, value := range record.Values { - if value != info.Value { - values = append(values, value) - } - } - - zr := internal.ZoneRecord{ - ZoneID: zoneID, - SpfTxtDomainID: zoneRecord.SpfTxtDomainID, - DomainName: info.EffectiveFQDN, - DomainTTL: d.config.TTL, - RecordType: "TXT", - Records: []internal.Record{{ - Values: values, - DomainID: zoneRecord.SpfTxtDomainID, - }}, - } - - err = d.client.UpdateZoneRecord(ctx, zr) - if err != nil { - return fmt.Errorf("manageengine: create zone record: %w", err) - } - - return nil - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -func (d *DNSProvider) findZoneID(ctx context.Context, authZone string) (int, error) { - zones, err := d.client.GetAllZones(ctx) - if err != nil { - return 0, fmt.Errorf("get all zone groups: %w", err) - } - - for _, zone := range zones { - if strings.EqualFold(zone.ZoneName, authZone) { - return zone.ZoneID, nil - } - } - - return 0, fmt.Errorf("zone not found %s", authZone) -} - -func (d *DNSProvider) findZoneRecord(ctx context.Context, zoneID int, fqdn string) (*internal.ZoneRecord, error) { - zoneRecords, err := d.client.GetAllZoneRecords(ctx, zoneID) - if err != nil { - return nil, fmt.Errorf("get all zone records: %w", err) - } - - for _, zoneRecord := range zoneRecords { - if !strings.EqualFold(zoneRecord.DomainName, fqdn) { - continue - } - - if strings.EqualFold(zoneRecord.RecordType, "TXT") { - return &zoneRecord, nil - } - } - - return nil, nil -} diff --git a/providers/dns/manageengine/manageengine.toml b/providers/dns/manageengine/manageengine.toml deleted file mode 100644 index 43a782841..000000000 --- a/providers/dns/manageengine/manageengine.toml +++ /dev/null @@ -1,23 +0,0 @@ -Name = "ManageEngine CloudDNS" -Description = '''''' -URL = "https://clouddns.manageengine.com" -Code = "manageengine" -Since = "v4.21.0" - -Example = ''' -MANAGEENGINE_CLIENT_ID="xxx" \ -MANAGEENGINE_CLIENT_SECRET="yyy" \ -lego --dns manageengine -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - MANAGEENGINE_CLIENT_ID = "Client ID" - MANAGEENGINE_CLIENT_SECRET = "Client Secret" - [Configuration.Additional] - MANAGEENGINE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - MANAGEENGINE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - MANAGEENGINE_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - -[Links] - API = "https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation" diff --git a/providers/dns/manageengine/manageengine_test.go b/providers/dns/manageengine/manageengine_test.go deleted file mode 100644 index 215de68dd..000000000 --- a/providers/dns/manageengine/manageengine_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package manageengine - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvClientID, EnvClientSecret).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvClientID: "abc", - EnvClientSecret: "secret", - }, - }, - { - desc: "missing client ID", - envVars: map[string]string{ - EnvClientID: "", - EnvClientSecret: "secret", - }, - expected: "manageengine: some credentials information are missing: MANAGEENGINE_CLIENT_ID", - }, - { - desc: "missing client secret", - envVars: map[string]string{ - EnvClientID: "abc", - EnvClientSecret: "", - }, - expected: "manageengine: some credentials information are missing: MANAGEENGINE_CLIENT_SECRET", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "manageengine: some credentials information are missing: MANAGEENGINE_CLIENT_ID,MANAGEENGINE_CLIENT_SECRET", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - clientID string - clientSecret string - expected string - }{ - { - desc: "success", - clientID: "abc", - clientSecret: "secret", - }, - { - desc: "missing client ID", - clientSecret: "secret", - expected: "manageengine: credentials missing", - }, - { - desc: "missing client secret", - clientID: "abc", - expected: "manageengine: credentials missing", - }, - { - desc: "missing credentials", - expected: "manageengine: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.ClientID = test.clientID - config.ClientSecret = test.clientSecret - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/manual/manual.go b/providers/dns/manual/manual.go deleted file mode 100644 index 2985bc595..000000000 --- a/providers/dns/manual/manual.go +++ /dev/null @@ -1,13 +0,0 @@ -package manual - -import ( - "github.com/go-acme/lego/v4/challenge/dns01" -) - -// DNSProvider is an implementation of the ChallengeProvider interface. -type DNSProvider = dns01.DNSProviderManual - -// NewDNSProvider returns a DNSProvider instance. -func NewDNSProvider() (*DNSProvider, error) { - return &DNSProvider{}, nil -} diff --git a/providers/dns/metaname/metaname.go b/providers/dns/metaname/metaname.go index d6e962024..ab5a4dff2 100644 --- a/providers/dns/metaname/metaname.go +++ b/providers/dns/metaname/metaname.go @@ -8,7 +8,6 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/nzdjb/go-metaname" @@ -26,8 +25,6 @@ const ( EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { AccountReference string @@ -79,7 +76,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config.AccountReference == "" { return nil, errors.New("metaname: missing account reference") } - if config.APIKey == "" { return nil, errors.New("metaname: missing api key") } @@ -153,10 +149,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("metaname: delete record: %w", err) } - d.recordsMu.Lock() - delete(d.records, token) - d.recordsMu.Unlock() - return nil } diff --git a/providers/dns/metaname/metaname.toml b/providers/dns/metaname/metaname.toml index 654dcaed0..bacdf9b6c 100644 --- a/providers/dns/metaname/metaname.toml +++ b/providers/dns/metaname/metaname.toml @@ -7,7 +7,7 @@ Since = "v4.13.0" Example = ''' METANAME_ACCOUNT_REFERENCE=xxxx \ METANAME_API_KEY=yyyyyyy \ -lego --dns metaname -d '*.example.com' -d example.com run +lego --email you@example.com --dns metaname --domains my.example.org run ''' [Configuration] @@ -15,9 +15,9 @@ lego --dns metaname -d '*.example.com' -d example.com run METANAME_ACCOUNT_REFERENCE = "The four-digit reference of a Metaname account" METANAME_API_KEY = "API Key" [Configuration.Additional] - METANAME_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - METANAME_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - METANAME_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + METANAME_POLLING_INTERVAL = "Time between DNS propagation check" + METANAME_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + METANAME_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://metaname.net/api/1.1/doc" diff --git a/providers/dns/metaname/metaname_test.go b/providers/dns/metaname/metaname_test.go index 855fc493d..174af4014 100644 --- a/providers/dns/metaname/metaname_test.go +++ b/providers/dns/metaname/metaname_test.go @@ -51,7 +51,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -123,7 +122,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -137,7 +135,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/metaregistrar/internal/client.go b/providers/dns/metaregistrar/internal/client.go deleted file mode 100644 index df99d81ba..000000000 --- a/providers/dns/metaregistrar/internal/client.go +++ /dev/null @@ -1,131 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" -) - -const defaultBaseURL = "https://api.metaregistrar.com" - -const tokenHeader = "token" - -// Client is a client to interact with the Metaregistrar API. -type Client struct { - token string - - baseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(token string) (*Client, error) { - if token == "" { - return nil, errors.New("token missing") - } - - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - token: token, - baseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -// UpdateDNSZone updates the DNS zone for a domain. -// To add or remove a TXT record we make a PATCH request. -// https://metaregistrar.dev/docu/metaapi/requests/patch_Update_dns_zone.html -func (c *Client) UpdateDNSZone(ctx context.Context, domain string, updateRequest DNSZoneUpdateRequest) (*DNSZoneUpdateResponse, error) { - endpoint := c.baseURL.JoinPath("dnszone", domain) - - req, err := newJSONRequest(ctx, http.MethodPatch, endpoint, updateRequest) - if err != nil { - return nil, err - } - - result := &DNSZoneUpdateResponse{} - - err = c.do(req, result) - if err != nil { - return nil, err - } - - return result, nil -} - -func (c *Client) do(req *http.Request, result any) error { - req.Header.Add(tokenHeader, c.token) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIError - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return &errAPI -} diff --git a/providers/dns/metaregistrar/internal/client_test.go b/providers/dns/metaregistrar/internal/client_test.go deleted file mode 100644 index 33e92cd7b..000000000 --- a/providers/dns/metaregistrar/internal/client_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("secret") - if err != nil { - return nil, err - } - - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - With(tokenHeader, "secret")) -} - -func TestClient_UpdateDNSZone(t *testing.T) { - client := mockBuilder(). - Route("PATCH /dnszone/example.com", - servermock.ResponseFromFixture("update-dns-zone.json"), - servermock.CheckRequestJSONBody(`{"add":[{"name":"@","type":"TXT","ttl":60,"content":"value"}]}`)). - Build(t) - - updateRequest := DNSZoneUpdateRequest{ - Add: []Record{{ - Name: "@", - Type: "TXT", - TTL: 60, - Content: "value", - }}, - } - - response, err := client.UpdateDNSZone(t.Context(), "example.com", updateRequest) - require.NoError(t, err) - - expected := &DNSZoneUpdateResponse{ - ResponseID: "mapi1_cb46ad8790b62b76535bd3102bd282aec83b894c", - Status: "ok", - Message: "Command completed successfully", - } - - assert.Equal(t, expected, response) -} - -func TestClient_UpdateDNSZone_error(t *testing.T) { - testCases := []struct { - desc string - filename string - expected string - }{ - { - desc: "authentication error", - filename: "error.json", - expected: "invalid_token: the supplied token is invalid", - }, - { - desc: "API error", - filename: "error-response.json", - expected: "error: does_not_exist: This server does not exist", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - client := mockBuilder(). - Route("PATCH /dnszone/example.com", - servermock.ResponseFromFixture(test.filename). - WithStatusCode(http.StatusUnprocessableEntity)). - Build(t) - - updateRequest := DNSZoneUpdateRequest{ - Add: []Record{{ - Name: "@", - Type: "TXT", - TTL: 60, - Content: "value", - }}, - } - - _, err := client.UpdateDNSZone(t.Context(), "example.com", updateRequest) - require.EqualError(t, err, test.expected) - }) - } -} diff --git a/providers/dns/metaregistrar/internal/fixtures/error-response.json b/providers/dns/metaregistrar/internal/fixtures/error-response.json deleted file mode 100644 index 8fa5a5ff3..000000000 --- a/providers/dns/metaregistrar/internal/fixtures/error-response.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "responseId": "1_0a407cb0634a56374ba80f863fda53ae37fd0042", - "status": "error", - "errorCode": "does_not_exist", - "errorMessage": "This server does not exist" -} diff --git a/providers/dns/metaregistrar/internal/fixtures/error.json b/providers/dns/metaregistrar/internal/fixtures/error.json deleted file mode 100644 index c76a32fc8..000000000 --- a/providers/dns/metaregistrar/internal/fixtures/error.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "error": "invalid_token", - "message": "the supplied token is invalid" -} \ No newline at end of file diff --git a/providers/dns/metaregistrar/internal/fixtures/update-dns-zone.json b/providers/dns/metaregistrar/internal/fixtures/update-dns-zone.json deleted file mode 100644 index b4977272a..000000000 --- a/providers/dns/metaregistrar/internal/fixtures/update-dns-zone.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "responseId": "mapi1_cb46ad8790b62b76535bd3102bd282aec83b894c", - "status": "ok", - "message": "Command completed successfully" -} diff --git a/providers/dns/metaregistrar/internal/types.go b/providers/dns/metaregistrar/internal/types.go deleted file mode 100644 index d8b6b3f87..000000000 --- a/providers/dns/metaregistrar/internal/types.go +++ /dev/null @@ -1,67 +0,0 @@ -package internal - -import ( - "strings" -) - -// APIError It's a mix of documented and undocumented fields. -// Note: the documentation is inconsistent: the names of property are not the same as the JSON sample. -// https://metaregistrar.dev/docu/metaapi/requests/response_ErrorResponse.html -type APIError struct { - ResponseID string `json:"responseId,omitempty"` - Status string `json:"status,omitempty"` - Message string `json:"message,omitempty"` - Err string `json:"error,omitempty"` - ErrorCode string `json:"errorCode,omitempty"` - ErrorMessage string `json:"errorMessage,omitempty"` -} - -func (e *APIError) Error() string { - var msg []string - - if e.Status != "" { - msg = append(msg, e.Status) - } - - if e.Err != "" { - msg = append(msg, e.Err) - } - - if e.ErrorCode != "" { - msg = append(msg, e.ErrorCode) - } - - if e.Message != "" { - msg = append(msg, e.Message) - } - - if e.ErrorMessage != "" { - msg = append(msg, e.ErrorMessage) - } - - return strings.Join(msg, ": ") -} - -type Record struct { - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` - TTL int `json:"ttl,omitempty"` - Content string `json:"content,omitempty"` - Priority int `json:"priority,omitempty"` - Disabled bool `json:"disabled,omitempty"` -} - -// DNSZoneUpdateRequest is the representation of DnszoneUpdateRequest object. -// https://metaregistrar.dev/docu/metaapi/requests/request_DnszoneUpdateRequest.html -type DNSZoneUpdateRequest struct { - Add []Record `json:"add,omitempty"` - Remove []Record `json:"rem,omitempty"` -} - -// DNSZoneUpdateResponse is the representation of DnszoneUpdateResponse object. -// https://metaregistrar.dev/docu/metaapi/requests/response_DnszoneUpdateResponse.html -type DNSZoneUpdateResponse struct { - ResponseID string `json:"responseId,omitempty"` - Status string `json:"status,omitempty"` - Message string `json:"message,omitempty"` -} diff --git a/providers/dns/metaregistrar/metaregistrar.go b/providers/dns/metaregistrar/metaregistrar.go deleted file mode 100644 index 7a601ef21..000000000 --- a/providers/dns/metaregistrar/metaregistrar.go +++ /dev/null @@ -1,150 +0,0 @@ -// Package metaregistrar implements a DNS provider for solving the DNS-01 challenge using Metaregistrar. -package metaregistrar - -import ( - "context" - "errors" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/metaregistrar/internal" -) - -// Environment variables names. -const ( - envNamespace = "METAREGISTRAR_" - - EnvToken = envNamespace + "API_TOKEN" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIToken string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for Metaregistrar. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvToken) - if err != nil { - return nil, fmt.Errorf("metaregistrar: %w", err) - } - - config := NewDefaultConfig() - config.APIToken = values[EnvToken] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Metaregistrar. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("metaregistrar: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.APIToken) - if err != nil { - return nil, fmt.Errorf("metaregistrar: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("metaregistrar: could not find zone for domain %q: %w", domain, err) - } - - updateRequest := internal.DNSZoneUpdateRequest{ - Add: []internal.Record{{ - Name: dns01.UnFqdn(info.EffectiveFQDN), - Type: "TXT", - TTL: d.config.TTL, - Content: info.Value, - }}, - } - - _, err = d.client.UpdateDNSZone(context.Background(), dns01.UnFqdn(authZone), updateRequest) - if err != nil { - return fmt.Errorf("metaregistrar: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("metaregistrar: could not find zone for domain %q: %w", domain, err) - } - - updateRequest := internal.DNSZoneUpdateRequest{ - Remove: []internal.Record{{ - Name: dns01.UnFqdn(info.EffectiveFQDN), - Type: "TXT", - TTL: d.config.TTL, - Content: strconv.Quote(info.Value), - }}, - } - - _, err = d.client.UpdateDNSZone(context.Background(), dns01.UnFqdn(authZone), updateRequest) - if err != nil { - return fmt.Errorf("metaregistrar: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/metaregistrar/metaregistrar.toml b/providers/dns/metaregistrar/metaregistrar.toml deleted file mode 100644 index e505e0ce2..000000000 --- a/providers/dns/metaregistrar/metaregistrar.toml +++ /dev/null @@ -1,22 +0,0 @@ -Name = "Metaregistrar" -Description = '''''' -URL = "https://metaregistrar.com/" -Code = "metaregistrar" -Since = "v4.23.0" - -Example = ''' -METAREGISTRAR_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns metaregistrar -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - METAREGISTRAR_API_TOKEN = "The API token" - [Configuration.Additional] - METAREGISTRAR_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - METAREGISTRAR_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - METAREGISTRAR_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - METAREGISTRAR_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://metaregistrar.dev/docu/metaapi/" diff --git a/providers/dns/metaregistrar/metaregistrar_test.go b/providers/dns/metaregistrar/metaregistrar_test.go deleted file mode 100644 index aa9bbbb58..000000000 --- a/providers/dns/metaregistrar/metaregistrar_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package metaregistrar - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvToken: "token", - }, - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "metaregistrar: some credentials information are missing: METAREGISTRAR_API_TOKEN", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiToken string - expected string - }{ - { - desc: "success", - apiToken: "token", - }, - { - desc: "missing credentials", - expected: "metaregistrar: token missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIToken = test.apiToken - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/mijnhost/internal/client.go b/providers/dns/mijnhost/internal/client.go deleted file mode 100644 index a51233211..000000000 --- a/providers/dns/mijnhost/internal/client.go +++ /dev/null @@ -1,163 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" -) - -const defaultBaseURL = "https://mijn.host/api/v2/" - -const authorizationHeader = "API-Key" - -// Client a mijn.host DNS API client. -type Client struct { - apiKey string - - baseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(apiKey string) *Client { - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - apiKey: apiKey, - baseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - } -} - -// ListDomains Retrieve all domains from an account. -// https://mijn.host/api/doc/api-3563872 -func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) { - endpoint := c.baseURL.JoinPath("domains") - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, fmt.Errorf("create request: %w", err) - } - - var results Response[DomainData] - - err = c.do(req, &results) - if err != nil { - return nil, err - } - - return results.Data.Domains, nil -} - -// GetRecords Retrieve DNS records of specific domain. -// https://mijn.host/api/doc/api-3563906 -func (c *Client) GetRecords(ctx context.Context, domain string) ([]Record, error) { - endpoint := c.baseURL.JoinPath("domains", domain, "dns") - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, fmt.Errorf("create request: %w", err) - } - - var results Response[RecordData] - - err = c.do(req, &results) - if err != nil { - return nil, err - } - - return results.Data.Records, nil -} - -// UpdateRecords Update DNS records of specific domain. -// https://mijn.host/api/doc/api-3563907 -func (c *Client) UpdateRecords(ctx context.Context, domain string, records []Record) error { - endpoint := c.baseURL.JoinPath("domains", domain, "dns") - - req, err := newJSONRequest(ctx, http.MethodPut, endpoint, RecordData{Records: records}) - if err != nil { - return fmt.Errorf("create request: %w", err) - } - - err = c.do(req, nil) - if err != nil { - return err - } - - return nil -} - -func (c *Client) do(req *http.Request, result any) error { - req.Header.Set(authorizationHeader, c.apiKey) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIError - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return &errAPI -} diff --git a/providers/dns/mijnhost/internal/client_test.go b/providers/dns/mijnhost/internal/client_test.go deleted file mode 100644 index 208616541..000000000 --- a/providers/dns/mijnhost/internal/client_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package internal - -import ( - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const apiKey = "secret" - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient(apiKey) - client.baseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - With(authorizationHeader, apiKey), - ) -} - -func TestClient_ListDomains(t *testing.T) { - client := mockBuilder(). - Route("GET /domains", servermock.ResponseFromFixture("list-domains.json")). - Build(t) - - domains, err := client.ListDomains(t.Context()) - require.NoError(t, err) - - expected := []Domain{{ - ID: 1000, - Domain: "example.com", - RenewalDate: "2030-01-01", - Status: "Active", - StatusID: 1, - Tags: []string{"my-tag"}, - }} - - assert.Equal(t, expected, domains) -} - -func TestClient_GetRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /domains/example.com/dns", servermock.ResponseFromFixture("get-dns-records.json")). - Build(t) - - records, err := client.GetRecords(t.Context(), "example.com") - require.NoError(t, err) - - expected := []Record{ - { - Type: "A", - Name: "example.com.", - Value: "135.226.123.12", - TTL: 900, - }, - { - Type: "AAAA", - Name: "example.com.", - Value: "2009:21d0:322:6100::5:c92b", - TTL: 900, - }, - { - Type: "MX", - Name: "example.com.", - Value: "10 mail.example.com.", - TTL: 900, - }, - { - Type: "TXT", - Name: "example.com.", - Value: "v=spf1 include:spf.mijn.host ~all", - TTL: 900, - }, - } - - assert.Equal(t, expected, records) -} - -func TestClient_UpdateRecords(t *testing.T) { - client := mockBuilder(). - Route("PUT /domains/example.com/dns", - servermock.ResponseFromFixture("update-dns-records.json"), - servermock.CheckRequestJSONBody(`{"records":[{"type":"TXT","name":"foo","value":"value1","ttl":120}]}`)). - Build(t) - - records := []Record{{ - Type: "TXT", - Name: "foo", - Value: "value1", - TTL: 120, - }} - - err := client.UpdateRecords(t.Context(), "example.com", records) - require.NoError(t, err) -} diff --git a/providers/dns/mijnhost/internal/fixtures/error.json b/providers/dns/mijnhost/internal/fixtures/error.json deleted file mode 100644 index fb7423a1d..000000000 --- a/providers/dns/mijnhost/internal/fixtures/error.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "status": 400, - "status_description": "Wrong request method" -} diff --git a/providers/dns/mijnhost/internal/fixtures/get-dns-records.json b/providers/dns/mijnhost/internal/fixtures/get-dns-records.json deleted file mode 100644 index 22db65fc6..000000000 --- a/providers/dns/mijnhost/internal/fixtures/get-dns-records.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "status": 200, - "status_description": "Request successful", - "data": { - "domain": "example.com", - "records": [ - { - "type": "A", - "name": "example.com.", - "value": "135.226.123.12", - "ttl": 900 - }, - { - "type": "AAAA", - "name": "example.com.", - "value": "2009:21d0:322:6100::5:c92b", - "ttl": 900 - }, - { - "type": "MX", - "name": "example.com.", - "value": "10 mail.example.com.", - "ttl": 900 - }, - { - "type": "TXT", - "name": "example.com.", - "value": "v=spf1 include:spf.mijn.host ~all", - "ttl": 900 - } - ] - } -} diff --git a/providers/dns/mijnhost/internal/fixtures/list-domains.json b/providers/dns/mijnhost/internal/fixtures/list-domains.json deleted file mode 100644 index b87b00668..000000000 --- a/providers/dns/mijnhost/internal/fixtures/list-domains.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "status": 200, - "status_description": "Request successful", - "data": { - "domains": [ - { - "id": 1000, - "domain": "example.com", - "renewal_date": "2030-01-01", - "status": "Active", - "status_id": 1, - "tags": [ - "my-tag" - ] - } - ] - } -} diff --git a/providers/dns/mijnhost/internal/fixtures/update-dns-records.json b/providers/dns/mijnhost/internal/fixtures/update-dns-records.json deleted file mode 100644 index 02155feaf..000000000 --- a/providers/dns/mijnhost/internal/fixtures/update-dns-records.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "status": 200, - "status_description": "DNS successfully updated" -} diff --git a/providers/dns/mijnhost/internal/types.go b/providers/dns/mijnhost/internal/types.go deleted file mode 100644 index aef3c33a4..000000000 --- a/providers/dns/mijnhost/internal/types.go +++ /dev/null @@ -1,43 +0,0 @@ -package internal - -import "fmt" - -type APIError struct { - Status int `json:"status,omitempty"` - StatusDescription string `json:"status_description,omitempty"` -} - -func (e APIError) Error() string { - return fmt.Sprintf("%d: %s", e.Status, e.StatusDescription) -} - -type Response[T any] struct { - Status int `json:"status,omitempty"` - StatusDescription string `json:"status_description,omitempty"` - Data T `json:"data,omitempty"` -} - -type RecordData struct { - Domain string `json:"domain,omitempty"` - Records []Record `json:"records,omitempty"` -} - -type Record struct { - Type string `json:"type,omitempty"` - Name string `json:"name,omitempty"` - Value string `json:"value,omitempty"` - TTL int `json:"ttl,omitempty"` -} - -type DomainData struct { - Domains []Domain `json:"domains"` -} - -type Domain struct { - ID int `json:"id"` - Domain string `json:"domain"` - RenewalDate string `json:"renewal_date"` - Status string `json:"status"` - StatusID int `json:"status_id"` - Tags []string `json:"tags"` -} diff --git a/providers/dns/mijnhost/mijnhost.go b/providers/dns/mijnhost/mijnhost.go deleted file mode 100644 index adb3e9ce3..000000000 --- a/providers/dns/mijnhost/mijnhost.go +++ /dev/null @@ -1,220 +0,0 @@ -// Package mijnhost implements a DNS provider for solving the DNS-01 challenge using mijn.host DNS. -package mijnhost - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/mijnhost/internal" -) - -// Environment variables names. -const ( - envNamespace = "MIJNHOST_" - - EnvAPIKey = envNamespace + "API_KEY" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -const txtType = "TXT" - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - TTL int - PropagationTimeout time.Duration - PollingInterval time.Duration - SequenceInterval time.Duration - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, 5*time.Second), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for mijn.host DNS. -// MIJNHOST_API_KEY must be passed in the environment variables. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIKey) - if err != nil { - return nil, fmt.Errorf("mijnhost: %w", err) - } - - config := NewDefaultConfig() - config.APIKey = values[EnvAPIKey] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for mijn.host DNS. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("mijnhost: the configuration of the DNS provider is nil") - } - - if config.APIKey == "" { - return nil, errors.New("mijnhost: APIKey is missing") - } - - client := internal.NewClient(config.APIKey) - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -// Sequential All DNS challenges for this provider will be resolved sequentially. -// Returns the interval between each iteration. -func (d *DNSProvider) Sequential() time.Duration { - return d.config.SequenceInterval -} - -// Present creates a TXT record to fulfill the dns-01 challenge. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - domains, err := d.client.ListDomains(ctx) - if err != nil { - return fmt.Errorf("mijnhost: list domains: %w", err) - } - - dom, err := findDomain(domains, domain) - if err != nil { - return fmt.Errorf("mijnhost: find domain: %w", err) - } - - records, err := d.client.GetRecords(ctx, dom.Domain) - if err != nil { - return fmt.Errorf("mijnhost: get records: %w", err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, dom.Domain) - if err != nil { - return fmt.Errorf("mijnhost: %w", err) - } - - record := internal.Record{ - Type: txtType, - Name: subDomain, - Value: info.Value, - TTL: d.config.TTL, - } - - // mijn.host doesn't support multiple values for a domain, - // so we removed existing record for the subdomain. - cleanedRecords := filterRecords(records, func(record internal.Record) bool { - return record.Type == txtType && (record.Name == subDomain || record.Name == dns01.UnFqdn(info.EffectiveFQDN)) - }) - - cleanedRecords = append(cleanedRecords, record) - - err = d.client.UpdateRecords(ctx, dom.Domain, cleanedRecords) - if err != nil { - return fmt.Errorf("mijnhost: update records: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - domains, err := d.client.ListDomains(ctx) - if err != nil { - return fmt.Errorf("mijnhost: list domains: %w", err) - } - - dom, err := findDomain(domains, domain) - if err != nil { - return fmt.Errorf("mijnhost: find domain: %w", err) - } - - records, err := d.client.GetRecords(ctx, dom.Domain) - if err != nil { - return fmt.Errorf("mijnhost: get records: %w", err) - } - - cleanedRecords := filterRecords(records, func(record internal.Record) bool { - return record.Type == txtType && record.Value == info.Value - }) - - err = d.client.UpdateRecords(ctx, dom.Domain, cleanedRecords) - if err != nil { - return fmt.Errorf("mijnhost: update records: %w", err) - } - - return nil -} - -func findDomain(domains []internal.Domain, fqdn string) (internal.Domain, error) { - for domain := range dns01.UnFqdnDomainsSeq(fqdn) { - for _, dom := range domains { - if dom.Domain == domain { - return dom, nil - } - } - } - - return internal.Domain{}, fmt.Errorf("domain %s not found", fqdn) -} - -func filterRecords(records []internal.Record, fn func(record internal.Record) bool) []internal.Record { - var newRecords []internal.Record - - for _, record := range records { - if record.Type == "TXT" && fn(record) { - continue - } - - newRecords = append(newRecords, record) - } - - return newRecords -} diff --git a/providers/dns/mijnhost/mijnhost.toml b/providers/dns/mijnhost/mijnhost.toml deleted file mode 100644 index 416fdde53..000000000 --- a/providers/dns/mijnhost/mijnhost.toml +++ /dev/null @@ -1,23 +0,0 @@ -Name = "mijn.host" -Description = '''''' -URL = "https://mijn.host/" -Code = "mijnhost" -Since = "v4.18.0" - -Example = ''' -MIJNHOST_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns mijnhost -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - MIJNHOST_API_KEY = "The API key" - [Configuration.Additional] - MIJNHOST_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - MIJNHOST_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - MIJNHOST_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 60)" - MIJNHOST_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - MIJNHOST_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://mijn.host/api/doc/" diff --git a/providers/dns/mijnhost/mijnhost_test.go b/providers/dns/mijnhost/mijnhost_test.go deleted file mode 100644 index c87ae0a40..000000000 --- a/providers/dns/mijnhost/mijnhost_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package mijnhost - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAPIKey: "key", - }, - }, - { - desc: "missing API key", - envVars: map[string]string{}, - expected: "mijnhost: some credentials information are missing: MIJNHOST_API_KEY", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiKey string - ttl int - expected string - }{ - { - desc: "success", - apiKey: "key", - }, - { - desc: "missing API key", - expected: "mijnhost: APIKey is missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIKey = test.apiKey - config.TTL = test.ttl - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/mittwald/internal/client.go b/providers/dns/mittwald/internal/client.go deleted file mode 100644 index 2b1564dc1..000000000 --- a/providers/dns/mittwald/internal/client.go +++ /dev/null @@ -1,211 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" -) - -const defaultBaseURL = "https://api.mittwald.de/v2/" - -const authorizationHeader = "Authorization" - -// Client the Mittwald client. -type Client struct { - token string - - baseURL *url.URL - HTTPClient *http.Client -} - -// NewClient Creates a new Client. -func NewClient(token string) *Client { - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - token: token, - baseURL: baseURL, - HTTPClient: &http.Client{Timeout: 5 * time.Second}, - } -} - -// ListDomains List Domains. -// https://api.mittwald.de/v2/docs/#/Domain/domain-list-domains -func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) { - endpoint := c.baseURL.JoinPath("domains") - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var result []Domain - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return result, nil -} - -// GetDNSZone Get a DNSZone. -// https://api.mittwald.de/v2/docs/#/Domain/dns-get-dns-zone -func (c *Client) GetDNSZone(ctx context.Context, zoneID string) (*DNSZone, error) { - endpoint := c.baseURL.JoinPath("dns-zones", zoneID) - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - result := &DNSZone{} - - err = c.do(req, result) - if err != nil { - return nil, err - } - - return result, nil -} - -// ListDNSZones List DNSZones belonging to a Project. -// https://api.mittwald.de/v2/docs/#/Domain/dns-list-dns-zones -func (c *Client) ListDNSZones(ctx context.Context, projectID string) ([]DNSZone, error) { - endpoint := c.baseURL.JoinPath("projects", projectID, "dns-zones") - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var result []DNSZone - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return result, nil -} - -// CreateDNSZone Create a DNSZone. -// https://api.mittwald.de/v2/docs/#/Domain/dns-create-dns-zone -func (c *Client) CreateDNSZone(ctx context.Context, zone CreateDNSZoneRequest) (*DNSZone, error) { - endpoint := c.baseURL.JoinPath("dns-zones") - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, zone) - if err != nil { - return nil, err - } - - result := &DNSZone{} - - err = c.do(req, result) - if err != nil { - return nil, err - } - - return result, nil -} - -// UpdateTXTRecord Update a record set on a DNSZone. -// https://api.mittwald.de/v2/docs/#/Domain/dns-update-record-set -func (c *Client) UpdateTXTRecord(ctx context.Context, zoneID string, record TXTRecord) error { - endpoint := c.baseURL.JoinPath("dns-zones", zoneID, "record-sets", "txt") - - req, err := newJSONRequest(ctx, http.MethodPut, endpoint, record) - if err != nil { - return err - } - - return c.do(req, nil) -} - -// DeleteDNSZone Delete a DNSZone. -// https://api.mittwald.de/v2/docs/#/Domain/dns-delete-dns-zone -func (c *Client) DeleteDNSZone(ctx context.Context, zoneID string) error { - endpoint := c.baseURL.JoinPath("dns-zones", zoneID) - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return err - } - - return c.do(req, nil) -} - -func (c *Client) do(req *http.Request, result any) error { - req.Header.Set(authorizationHeader, "Bearer "+c.token) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var response APIError - - err := json.Unmarshal(raw, &response) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return fmt.Errorf("[status code %d] %w", resp.StatusCode, response) -} diff --git a/providers/dns/mittwald/internal/client_test.go b/providers/dns/mittwald/internal/client_test.go deleted file mode 100644 index e57c80f7a..000000000 --- a/providers/dns/mittwald/internal/client_test.go +++ /dev/null @@ -1,158 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("secret") - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("Bearer secret"), - ) -} - -func TestClient_ListDomains(t *testing.T) { - client := mockBuilder(). - Route("GET /domains", servermock.ResponseFromFixture("domain-list-domains.json")). - Build(t) - - domains, err := client.ListDomains(t.Context()) - require.NoError(t, err) - - require.Len(t, domains, 1) - - expected := []Domain{{ - Domain: "string", - DomainID: "3fa85f64-5717-4562-b3fc-2c963f66afa6", - ProjectID: "3fa85f64-5717-4562-b3fc-2c963f66afa6", - }} - - assert.Equal(t, expected, domains) -} - -func TestClient_ListDomains_error(t *testing.T) { - client := mockBuilder(). - Route("GET /domains", - servermock.ResponseFromFixture("error-client.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) - - _, err := client.ListDomains(t.Context()) - require.EqualError(t, err, "[status code 400] ValidationError: Validation failed [format: should be string (.address.street, email)]") -} - -func TestClient_ListDNSZones(t *testing.T) { - client := mockBuilder(). - Route("GET /projects/my-project-id/dns-zones", servermock.ResponseFromFixture("dns-list-dns-zones.json")). - Build(t) - - zones, err := client.ListDNSZones(t.Context(), "my-project-id") - require.NoError(t, err) - - require.Len(t, zones, 1) - - expected := []DNSZone{{ - ID: "3fa85f64-5717-4562-b3fc-2c963f66afa6", - Domain: "string", - RecordSet: &RecordSet{ - TXT: &TXTRecord{}, - }, - }} - - assert.Equal(t, expected, zones) -} - -func TestClient_GetDNSZone(t *testing.T) { - client := mockBuilder(). - Route("GET /dns-zones/my-zone-id", servermock.ResponseFromFixture("dns-get-dns-zone.json")). - Build(t) - - zone, err := client.GetDNSZone(t.Context(), "my-zone-id") - require.NoError(t, err) - - expected := &DNSZone{ - ID: "3fa85f64-5717-4562-b3fc-2c963f66afa6", - Domain: "string", - RecordSet: &RecordSet{ - TXT: &TXTRecord{}, - }, - } - - assert.Equal(t, expected, zone) -} - -func TestClient_CreateDNSZone(t *testing.T) { - client := mockBuilder(). - Route("POST /dns-zones", - servermock.ResponseFromFixture("dns-create-dns-zone.json"), - servermock.CheckRequestJSONBody(`{"name":"test","parentZoneId":"my-parent-zone-id"}`)). - Build(t) - - request := CreateDNSZoneRequest{ - Name: "test", - ParentZoneID: "my-parent-zone-id", - } - - zone, err := client.CreateDNSZone(t.Context(), request) - require.NoError(t, err) - - expected := &DNSZone{ - ID: "3fa85f64-5717-4562-b3fc-2c963f66afa6", - } - - assert.Equal(t, expected, zone) -} - -func TestClient_UpdateTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("PUT /dns-zones/my-zone-id/record-sets/txt", - servermock.Noop(). - WithStatusCode(http.StatusNoContent), - servermock.CheckRequestJSONBody(`{"settings":{"ttl":{"auto":true}},"entries":["txt"]}`)). - Build(t) - - record := TXTRecord{ - Settings: Settings{ - TTL: TTL{Auto: true}, - }, - Entries: []string{"txt"}, - } - - err := client.UpdateTXTRecord(t.Context(), "my-zone-id", record) - require.NoError(t, err) -} - -func TestClient_DeleteDNSZone(t *testing.T) { - client := mockBuilder(). - Route("DELETE /dns-zones/my-zone-id", - servermock.Noop()). - Build(t) - - err := client.DeleteDNSZone(t.Context(), "my-zone-id") - require.NoError(t, err) -} - -func TestClient_DeleteDNSZone_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /dns-zones/my-zone-id", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusInternalServerError)). - Build(t) - - err := client.DeleteDNSZone(t.Context(), "my-zone-id") - assert.EqualError(t, err, "[status code 500] InternalServerError: Something went wrong") -} diff --git a/providers/dns/mittwald/internal/fixtures/dns-create-dns-zone.json b/providers/dns/mittwald/internal/fixtures/dns-create-dns-zone.json deleted file mode 100644 index ef924d5e8..000000000 --- a/providers/dns/mittwald/internal/fixtures/dns-create-dns-zone.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" -} diff --git a/providers/dns/mittwald/internal/fixtures/dns-get-dns-zone.json b/providers/dns/mittwald/internal/fixtures/dns-get-dns-zone.json deleted file mode 100644 index 743cf2f04..000000000 --- a/providers/dns/mittwald/internal/fixtures/dns-get-dns-zone.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "domain": "string", - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "recordSet": { - "cname": {}, - "combinedARecords": {}, - "mx": {}, - "srv": {}, - "txt": {} - } -} diff --git a/providers/dns/mittwald/internal/fixtures/dns-list-dns-zones.json b/providers/dns/mittwald/internal/fixtures/dns-list-dns-zones.json deleted file mode 100644 index 7b5fd08cb..000000000 --- a/providers/dns/mittwald/internal/fixtures/dns-list-dns-zones.json +++ /dev/null @@ -1,13 +0,0 @@ -[ - { - "domain": "string", - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "recordSet": { - "cname": {}, - "combinedARecords": {}, - "mx": {}, - "srv": {}, - "txt": {} - } - } -] diff --git a/providers/dns/mittwald/internal/fixtures/domain-list-domains.json b/providers/dns/mittwald/internal/fixtures/domain-list-domains.json deleted file mode 100644 index 520830867..000000000 --- a/providers/dns/mittwald/internal/fixtures/domain-list-domains.json +++ /dev/null @@ -1,74 +0,0 @@ -[ - { - "authCode": { - "expires": "2024-06-04T15:11:59.964Z", - "value": "string" - }, - "authCode2": { - "expires": "2024-06-04T15:11:59.964Z" - }, - "connected": true, - "deleted": true, - "domain": "string", - "domainId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "handles": { - "adminC": { - "current": { - "handleFields": [ - { - "name": "string", - "value": "jnoFDyCBDHC&70Zp&2JMErZBq(),fnsYIvn_bOed5e_.vmsrZ3-IH )Ms)Xc13KDWy2WMH((mJ.-uY_NEBu/3MO8)3" - } - ], - "handleRef": "string" - }, - "desired": { - "handleFields": [ - { - "name": "string", - "value": "1odACmUIyjG Xa-uEX7R+f4,ykqpZ71FFLzkl8B87/+I@s0bVMxA" - } - ], - "handleRef": "string" - } - }, - "ownerC": { - "current": { - "handleFields": [ - { - "name": "string", - "value": "oklq/PU.yBrSFq) .Qx_Uqb8NBZnwA(9jk@x4w Dp6lLd&+a-A.oG5sHw(jcRSOyv0" - } - ], - "handleRef": "string" - }, - "desired": { - "handleFields": [ - { - "name": "string", - "value": "iwt.q,vygqXwZ0_HK+j3kuw/,A,Z)L1Jg&fNgIxWdBc1xnGj(pjj8YQX1DG 9M1/_Vaam," - } - ], - "handleRef": "string" - } - } - }, - "nameservers": [ - "string" - ], - "processes": [ - { - "error": "string", - "lastUpdate": "2024-06-04T15:11:59.973Z", - "processType": "UNSPECIFIED", - "state": "UNSPECIFIED", - "status": "string", - "statusCode": "string", - "transactionId": "string" - } - ], - "projectId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "transferInAuthCode": "string", - "usesDefaultNameserver": true - } -] diff --git a/providers/dns/mittwald/internal/fixtures/error-client.json b/providers/dns/mittwald/internal/fixtures/error-client.json deleted file mode 100644 index d62f80678..000000000 --- a/providers/dns/mittwald/internal/fixtures/error-client.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "ValidationError", - "message": "Validation failed", - "validationErrors": [ - { - "message": "should be string", - "path": ".address.street", - "type": "format", - "context": { - "format": "email" - } - } - ] -} diff --git a/providers/dns/mittwald/internal/fixtures/error.json b/providers/dns/mittwald/internal/fixtures/error.json deleted file mode 100644 index 00919f64f..000000000 --- a/providers/dns/mittwald/internal/fixtures/error.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "message": "Something went wrong", - "type": "InternalServerError" -} diff --git a/providers/dns/mittwald/internal/types.go b/providers/dns/mittwald/internal/types.go deleted file mode 100644 index 86cdf065c..000000000 --- a/providers/dns/mittwald/internal/types.go +++ /dev/null @@ -1,87 +0,0 @@ -package internal - -import ( - "fmt" - "strings" -) - -// https://api.mittwald.de/v2/docs/#/Domain/domain-list-domains - -type Domain struct { - Domain string `json:"domain,omitempty"` - DomainID string `json:"domainId,omitempty"` - ProjectID string `json:"projectId,omitempty"` -} - -// https://api.mittwald.de/v2/docs/#/Domain/dns-list-dns-zones - -type DNSZone struct { - ID string `json:"id,omitempty"` - Domain string `json:"domain,omitempty"` - RecordSet *RecordSet `json:"recordSet,omitempty"` -} - -type RecordSet struct { - TXT *TXTRecord `json:"txt"` -} - -// https://api.mittwald.de/v2/docs/#/Domain/dns-create-dns-zone - -type CreateDNSZoneRequest struct { - Name string `json:"name,omitempty"` - ParentZoneID string `json:"parentZoneId,omitempty"` -} - -type NewDNSZone struct { - ID string `json:"id"` -} - -// https://api.mittwald.de/v2/docs/#/Domain/dns-update-record-set - -type TXTRecord struct { - Settings Settings `json:"settings"` - Entries []string `json:"entries,omitempty"` -} - -type Settings struct { - TTL TTL `json:"ttl"` -} - -type TTL struct { - Seconds int `json:"seconds,omitempty"` - Auto bool `json:"auto,omitempty"` -} - -// Error - -type APIError struct { - Type string `json:"type,omitempty"` - Message string `json:"message,omitempty"` - ValidationErrors []ValidationError `json:"validationErrors,omitempty"` -} - -func (a APIError) Error() string { - msg := new(strings.Builder) - - _, _ = fmt.Fprintf(msg, "%s: %s", a.Type, a.Message) - - if len(a.ValidationErrors) > 0 { - for _, validationError := range a.ValidationErrors { - _, _ = fmt.Fprintf(msg, " [%s: %s (%s, %s)]", - validationError.Type, validationError.Message, validationError.Path, validationError.Context.Format) - } - } - - return msg.String() -} - -type ValidationError struct { - Message string `json:"message,omitempty"` - Path string `json:"path,omitempty"` - Type string `json:"type,omitempty"` - Context ValidationErrorContext `json:"context"` -} - -type ValidationErrorContext struct { - Format string `json:"format,omitempty"` -} diff --git a/providers/dns/mittwald/mittwald.go b/providers/dns/mittwald/mittwald.go deleted file mode 100644 index dcd882482..000000000 --- a/providers/dns/mittwald/mittwald.go +++ /dev/null @@ -1,249 +0,0 @@ -// Package mittwald implements a DNS provider for solving the DNS-01 challenge using Mittwald. -package mittwald - -import ( - "context" - "errors" - "fmt" - "net/http" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/mittwald/internal" -) - -// Environment variables names. -const ( - envNamespace = "MITTWALD_" - - EnvToken = envNamespace + "TOKEN" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -const minTTL = 300 - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - Token string - TTL int - PropagationTimeout time.Duration - PollingInterval time.Duration - SequenceInterval time.Duration - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, minTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), - SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, 2*time.Minute), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client - - zoneIDs map[string]string - zoneIDsMu sync.Mutex -} - -// NewDNSProvider returns a DNSProvider instance configured for Mittwald. -// Credentials must be passed in the environment variables: MITTWALD_TOKEN. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvToken) - if err != nil { - return nil, fmt.Errorf("mittwald: %w", err) - } - - config := NewDefaultConfig() - config.Token = values[EnvToken] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Mittwald. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("mittwald: the configuration of the DNS provider is nil") - } - - if config.Token == "" { - return nil, errors.New("mittwald: some credentials information are missing") - } - - if config.TTL < minTTL { - return nil, fmt.Errorf("mittwald: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) - } - - client := internal.NewClient(config.Token) - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - zoneIDs: map[string]string{}, - }, nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -// Sequential All DNS challenges for this provider will be resolved sequentially. -// Returns the interval between each iteration. -func (d *DNSProvider) Sequential() time.Duration { - return d.config.SequenceInterval -} - -// Present creates a TXT record to fulfill the dns-01 challenge. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) - - zone, err := d.getOrCreateZone(ctx, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("mittwald: get effective zone: %w", err) - } - - record := internal.TXTRecord{ - Settings: internal.Settings{ - TTL: internal.TTL{Seconds: d.config.TTL}, - }, - Entries: []string{info.Value}, - } - - err = d.client.UpdateTXTRecord(ctx, zone.ID, record) - if err != nil { - return fmt.Errorf("mittwald: update/add TXT record: %w", err) - } - - d.zoneIDsMu.Lock() - d.zoneIDs[token] = zone.ID - d.zoneIDsMu.Unlock() - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) - - // get the record's unique ID from when we created it - d.zoneIDsMu.Lock() - zoneID, ok := d.zoneIDs[token] - d.zoneIDsMu.Unlock() - - if !ok { - return fmt.Errorf("mittwald: unknown zone ID for '%s'", info.EffectiveFQDN) - } - - record := internal.TXTRecord{Entries: make([]string, 0)} - - err := d.client.UpdateTXTRecord(ctx, zoneID, record) - if err != nil { - return fmt.Errorf("mittwald: update/delete TXT record: %w", err) - } - - d.zoneIDsMu.Lock() - delete(d.zoneIDs, token) - d.zoneIDsMu.Unlock() - - return nil -} - -func (d *DNSProvider) getOrCreateZone(ctx context.Context, fqdn string) (*internal.DNSZone, error) { - domains, err := d.client.ListDomains(ctx) - if err != nil { - return nil, fmt.Errorf("list domains: %w", err) - } - - dom, err := findDomain(domains, fqdn) - if err != nil { - return nil, fmt.Errorf("find domain: %w", err) - } - - zones, err := d.client.ListDNSZones(ctx, dom.ProjectID) - if err != nil { - return nil, fmt.Errorf("list DNS zones: %w", err) - } - - for _, zone := range zones { - if zone.Domain == dns01.UnFqdn(fqdn) { - return &zone, nil - } - } - - // Looking for parent zone to create a new zone for the subdomain. - - parentZone, err := findZone(zones, fqdn) - if err != nil { - return nil, fmt.Errorf("find zone: %w", err) - } - - subDomain, err := dns01.ExtractSubDomain(fqdn, parentZone.Domain) - if err != nil { - return nil, err - } - - request := internal.CreateDNSZoneRequest{ - Name: subDomain, - ParentZoneID: parentZone.ID, - } - - zone, err := d.client.CreateDNSZone(ctx, request) - if err != nil { - return nil, fmt.Errorf("create DNS zone: %w", err) - } - - return zone, nil -} - -func findDomain(domains []internal.Domain, fqdn string) (internal.Domain, error) { - for domain := range dns01.UnFqdnDomainsSeq(fqdn) { - for _, dom := range domains { - if dom.Domain == domain { - return dom, nil - } - } - } - - return internal.Domain{}, fmt.Errorf("domain %s not found", fqdn) -} - -func findZone(zones []internal.DNSZone, fqdn string) (internal.DNSZone, error) { - for domain := range dns01.UnFqdnDomainsSeq(fqdn) { - for _, zon := range zones { - if zon.Domain == domain { - return zon, nil - } - } - } - - return internal.DNSZone{}, fmt.Errorf("zone %s not found", fqdn) -} diff --git a/providers/dns/mittwald/mittwald.toml b/providers/dns/mittwald/mittwald.toml deleted file mode 100644 index 36a9f6c16..000000000 --- a/providers/dns/mittwald/mittwald.toml +++ /dev/null @@ -1,23 +0,0 @@ -Name = "Mittwald" -Description = '''''' -URL = "https://www.mittwald.de/" -Code = "mittwald" -Since = "v1.48.0" - -Example = ''' -MITTWALD_TOKEN=my-token \ -lego --dns mittwald -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - MITTWALD_TOKEN = "API token" - [Configuration.Additional] - MITTWALD_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" - MITTWALD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - MITTWALD_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - MITTWALD_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 120)" - MITTWALD_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://api.mittwald.de/v2/docs/" diff --git a/providers/dns/mittwald/mittwald_test.go b/providers/dns/mittwald/mittwald_test.go deleted file mode 100644 index 6a6599536..000000000 --- a/providers/dns/mittwald/mittwald_test.go +++ /dev/null @@ -1,246 +0,0 @@ -package mittwald - -import ( - "testing" - "time" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/providers/dns/mittwald/internal" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvToken: "secret", - }, - }, - { - desc: "missing credentials", - envVars: map[string]string{ - EnvToken: "", - }, - expected: "mittwald: some credentials information are missing: MITTWALD_TOKEN", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - assert.NoError(t, err) - assert.NotNil(t, p) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - token string - ttl int - expected string - }{ - { - desc: "success", - token: "secret", - }, - { - desc: "missing credentials", - expected: "mittwald: some credentials information are missing", - }, - { - desc: "invalid TTL", - token: "secret", - ttl: 10, - expected: "mittwald: invalid TTL, TTL (10) must be greater than 300", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.Token = test.token - - if test.ttl > 0 { - config.TTL = test.ttl - } - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - assert.NoError(t, err) - assert.NotNil(t, p) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - time.Sleep(2 * time.Second) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func Test_findDomain(t *testing.T) { - domains := []internal.Domain{ - { - Domain: "example.com", - ProjectID: "a1", - }, - { - Domain: "foo.example.com", - ProjectID: "a2", - }, - { - Domain: "example.org", - ProjectID: "b1", - }, - { - Domain: "foo.example.org", - ProjectID: "b2", - }, - { - Domain: "test.example.org", - ProjectID: "b3", - }, - } - - testCases := []struct { - desc string - fqdn string - expected internal.Domain - }{ - { - desc: "exact match", - fqdn: "example.org.", - expected: internal.Domain{Domain: "example.org", ProjectID: "b1"}, - }, - { - desc: "1 level parent", - fqdn: "_acme-challenge.test.example.org.", - expected: internal.Domain{Domain: "test.example.org", ProjectID: "b3"}, - }, - { - desc: "2 levels parent", - fqdn: "_acme-challenge.test.example.com.", - expected: internal.Domain{Domain: "example.com", ProjectID: "a1"}, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - domain, err := findDomain(domains, test.fqdn) - require.NoError(t, err) - - assert.Equal(t, test.expected, domain) - }) - } -} - -func Test_findZone(t *testing.T) { - zones := []internal.DNSZone{ - { - Domain: "example.com", - ID: "a1", - }, - { - Domain: "foo.example.com", - ID: "a2", - }, - { - Domain: "example.org", - ID: "b1", - }, - { - Domain: "foo.example.org", - ID: "b2", - }, - { - Domain: "test.example.org", - ID: "b3", - }, - } - - testCases := []struct { - desc string - fqdn string - expected internal.DNSZone - }{ - { - desc: "exact match", - fqdn: "example.org.", - expected: internal.DNSZone{Domain: "example.org", ID: "b1"}, - }, - { - desc: "1 level parent", - fqdn: "_acme-challenge.test.example.org.", - expected: internal.DNSZone{Domain: "test.example.org", ID: "b3"}, - }, - { - desc: "2 levels parent", - fqdn: "_acme-challenge.test.example.com.", - expected: internal.DNSZone{Domain: "example.com", ID: "a1"}, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - zone, err := findZone(zones, test.fqdn) - require.NoError(t, err) - - assert.Equal(t, test.expected, zone) - }) - } -} diff --git a/providers/dns/myaddr/internal/client.go b/providers/dns/myaddr/internal/client.go deleted file mode 100644 index 40f919c7d..000000000 --- a/providers/dns/myaddr/internal/client.go +++ /dev/null @@ -1,115 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "sync" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" -) - -const defaultBaseURL = "https://myaddr.tools" - -// Client the myaddr.{tools,dev,io} API client. -type Client struct { - baseURL *url.URL - HTTPClient *http.Client - - credentials map[string]string - credMu sync.Mutex -} - -// NewClient creates a new Client. -func NewClient(credentials map[string]string) (*Client, error) { - if len(credentials) == 0 { - return nil, errors.New("credentials missing") - } - - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - baseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - credentials: credentials, - }, nil -} - -func (c *Client) AddTXTRecord(ctx context.Context, subdomain, value string) error { - c.credMu.Lock() - privateKey, ok := c.credentials[subdomain] - c.credMu.Unlock() - - if !ok { - return fmt.Errorf("subdomain %s not found in credentials, check your credentials map", subdomain) - } - - payload := ACMEChallenge{Key: privateKey, Data: value} - - req, err := newJSONRequest(ctx, http.MethodPost, c.baseURL.JoinPath("update"), payload) - if err != nil { - return err - } - - return c.do(req, nil) -} - -func (c *Client) do(req *http.Request, result any) error { - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - raw, _ := io.ReadAll(resp.Body) - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} diff --git a/providers/dns/myaddr/internal/client_test.go b/providers/dns/myaddr/internal/client_test.go deleted file mode 100644 index 36506d94a..000000000 --- a/providers/dns/myaddr/internal/client_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - credentials := map[string]string{ - "example": "secret", - } - - client, err := NewClient(credentials) - if err != nil { - return nil, err - } - - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(), - ) -} - -func TestClient_AddTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /update", nil, - servermock.CheckRequestJSONBody(`{"key":"secret","acme_challenge":"txt"}`)). - Build(t) - - err := client.AddTXTRecord(t.Context(), "example", "txt") - require.NoError(t, err) -} - -func TestClient_AddTXTRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /update", - servermock.ResponseFromFixture("error.txt"). - WithStatusCode(http.StatusBadRequest)). - Build(t) - - err := client.AddTXTRecord(t.Context(), "example", "txt") - require.EqualError(t, err, `unexpected status code: [status code: 400] body: invalid value for "key"`) -} - -func TestClient_AddTXTRecord_error_credentials(t *testing.T) { - client := mockBuilder(). - Route("POST /update", nil). - Build(t) - - err := client.AddTXTRecord(t.Context(), "nx", "txt") - require.EqualError(t, err, "subdomain nx not found in credentials, check your credentials map") -} diff --git a/providers/dns/myaddr/internal/fixtures/error.txt b/providers/dns/myaddr/internal/fixtures/error.txt deleted file mode 100644 index 64a417673..000000000 --- a/providers/dns/myaddr/internal/fixtures/error.txt +++ /dev/null @@ -1 +0,0 @@ -invalid value for "key" diff --git a/providers/dns/myaddr/internal/types.go b/providers/dns/myaddr/internal/types.go deleted file mode 100644 index 36f057497..000000000 --- a/providers/dns/myaddr/internal/types.go +++ /dev/null @@ -1,6 +0,0 @@ -package internal - -type ACMEChallenge struct { - Key string `json:"key"` - Data string `json:"acme_challenge"` -} diff --git a/providers/dns/myaddr/myaddr.go b/providers/dns/myaddr/myaddr.go deleted file mode 100644 index fb7ea66a0..000000000 --- a/providers/dns/myaddr/myaddr.go +++ /dev/null @@ -1,147 +0,0 @@ -// Package myaddr implements a DNS provider for solving the DNS-01 challenge using myaddr.{tools,dev,io}. -package myaddr - -import ( - "context" - "errors" - "fmt" - "net/http" - "strings" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/myaddr/internal" -) - -// Environment variables names. -const ( - envNamespace = "MYADDR_" - - EnvPrivateKeysMapping = envNamespace + "PRIVATE_KEYS_MAPPING" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - Credentials map[string]string - - PropagationTimeout time.Duration - PollingInterval time.Duration - SequenceInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for myaddr.{tools,dev,io}. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvPrivateKeysMapping) - if err != nil { - return nil, fmt.Errorf("myaddr: %w", err) - } - - config := NewDefaultConfig() - - credentials, err := env.ParsePairs(values[EnvPrivateKeysMapping]) - if err != nil { - return nil, fmt.Errorf("myaddr: %w", err) - } - - config.Credentials = credentials - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for myaddr.{tools,dev,io}. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("myaddr: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.Credentials) - if err != nil { - return nil, fmt.Errorf("myaddr: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("myaddr: could not find zone for domain %q: %w", domain, err) - } - - fullSubdomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("myaddr: %w", err) - } - - _, after, found := strings.Cut(fullSubdomain, ".") - if !found { - return fmt.Errorf("myaddr: subdomain not found in: %q (%s)", fullSubdomain, info.EffectiveFQDN) - } - - err = d.client.AddTXTRecord(context.Background(), after, info.Value) - if err != nil { - return fmt.Errorf("myaddr: add TXT record: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - // There is no API endpoint to delete a TXT record: - // TXT records are automatically removed after a few minutes. - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -// Sequential All DNS challenges for this provider will be resolved sequentially. -// Returns the interval between each iteration. -func (d *DNSProvider) Sequential() time.Duration { - return d.config.SequenceInterval -} diff --git a/providers/dns/myaddr/myaddr.toml b/providers/dns/myaddr/myaddr.toml deleted file mode 100644 index 2f5fe6c1f..000000000 --- a/providers/dns/myaddr/myaddr.toml +++ /dev/null @@ -1,23 +0,0 @@ -Name = "myaddr.{tools,dev,io}" -Description = '''''' -URL = "https://myaddr.tools/" -Code = "myaddr" -Since = "v4.22.0" - -Example = ''' -MYADDR_PRIVATE_KEYS_MAPPING="example:123,test:456" \ -lego --dns myaddr -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - MYADDR_PRIVATE_KEYS_MAPPING = "Mapping between subdomains and private keys. The format is: `:,:,:`" - [Configuration.Additional] - MYADDR_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - MYADDR_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - MYADDR_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 2)" - MYADDR_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - MYADDR_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://myaddr.tools/" diff --git a/providers/dns/mydnsjp/internal/client.go b/providers/dns/mydnsjp/internal/client.go index 20469d657..9859ed685 100644 --- a/providers/dns/mydnsjp/internal/client.go +++ b/providers/dns/mydnsjp/internal/client.go @@ -23,7 +23,7 @@ type Client struct { } // NewClient Creates a new Client. -func NewClient(masterID, password string) *Client { +func NewClient(masterID string, password string) *Client { baseURL, _ := url.Parse(defaultBaseURL) return &Client{ @@ -34,15 +34,15 @@ func NewClient(masterID, password string) *Client { } } -func (c *Client) AddTXTRecord(ctx context.Context, domain, value string) error { +func (c Client) AddTXTRecord(ctx context.Context, domain, value string) error { return c.doRequest(ctx, domain, value, "REGIST") } -func (c *Client) DeleteTXTRecord(ctx context.Context, domain, value string) error { +func (c Client) DeleteTXTRecord(ctx context.Context, domain, value string) error { return c.doRequest(ctx, domain, value, "DELETE") } -func (c *Client) buildRequest(ctx context.Context, domain, value, cmd string) (*http.Request, error) { +func (c Client) buildRequest(ctx context.Context, domain, value, cmd string) (*http.Request, error) { params := url.Values{} params.Set("CERTBOT_DOMAIN", domain) params.Set("CERTBOT_VALIDATION", value) @@ -58,7 +58,7 @@ func (c *Client) buildRequest(ctx context.Context, domain, value, cmd string) (* return req, nil } -func (c *Client) doRequest(ctx context.Context, domain, value, cmd string) error { +func (c Client) doRequest(ctx context.Context, domain, value, cmd string) error { req, err := c.buildRequest(ctx, domain, value, cmd) if err != nil { return err diff --git a/providers/dns/mydnsjp/internal/client_test.go b/providers/dns/mydnsjp/internal/client_test.go index 41ccbba87..a68f6888b 100644 --- a/providers/dns/mydnsjp/internal/client_test.go +++ b/providers/dns/mydnsjp/internal/client_test.go @@ -1,50 +1,92 @@ package internal import ( + "context" + "fmt" + "net/http" "net/http/httptest" "net/url" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("xxx", "secret") - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) +func setupTest(t *testing.T, cmdName string) *Client { + t.Helper() - return client, nil - }, - servermock.CheckHeader(). - WithContentTypeFromURLEncoded(). - WithBasicAuth("xxx", "secret")) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("invalid method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + username, password, ok := req.BasicAuth() + if !ok { + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + if username != "xxx" { + http.Error(rw, fmt.Sprintf("username: want %s got %s", username, "xxx"), http.StatusUnauthorized) + return + } + + if password != "secret" { + http.Error(rw, fmt.Sprintf("password: want %s got %s", password, "secret"), http.StatusUnauthorized) + return + } + + if req.Header.Get("Content-Type") != "application/x-www-form-urlencoded" { + http.Error(rw, fmt.Sprintf("invalid Content-Type: %s", req.Header.Get("Content-Type")), http.StatusBadRequest) + return + } + + err := req.ParseForm() + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + domain := req.Form.Get("CERTBOT_DOMAIN") + if domain != "example.com" { + http.Error(rw, fmt.Sprintf("unexpected CERTBOT_DOMAIN: %s", domain), http.StatusBadRequest) + return + } + + validation := req.Form.Get("CERTBOT_VALIDATION") + if validation != "txt" { + http.Error(rw, fmt.Sprintf("unexpected CERTBOT_VALIDATION: %s", validation), http.StatusBadRequest) + return + } + + cmd := req.Form.Get("EDIT_CMD") + if cmd != cmdName { + http.Error(rw, fmt.Sprintf("unexpected EDIT_CMD: %s", cmd), http.StatusBadRequest) + return + } + }) + + client := NewClient("xxx", "secret") + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client } func TestClient_AddTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /", nil, - servermock.CheckForm().Strict(). - With("CERTBOT_DOMAIN", "example.com"). - With("CERTBOT_VALIDATION", "txt"). - With("EDIT_CMD", "REGIST")). - Build(t) + client := setupTest(t, "REGIST") - err := client.AddTXTRecord(t.Context(), "example.com", "txt") + err := client.AddTXTRecord(context.Background(), "example.com", "txt") require.NoError(t, err) } func TestClient_DeleteTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /", nil, - servermock.CheckForm().Strict(). - With("CERTBOT_DOMAIN", "example.com"). - With("CERTBOT_VALIDATION", "txt"). - With("EDIT_CMD", "DELETE")). - Build(t) + client := setupTest(t, "DELETE") - err := client.DeleteTXTRecord(t.Context(), "example.com", "txt") + err := client.DeleteTXTRecord(context.Background(), "example.com", "txt") require.NoError(t, err) } diff --git a/providers/dns/mydnsjp/mydnsjp.go b/providers/dns/mydnsjp/mydnsjp.go index 8a790c88e..beaaf49ae 100644 --- a/providers/dns/mydnsjp/mydnsjp.go +++ b/providers/dns/mydnsjp/mydnsjp.go @@ -8,10 +8,8 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/mydnsjp/internal" ) @@ -27,8 +25,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { MasterID string @@ -42,7 +38,7 @@ type Config struct { func NewDefaultConfig() *Config { return &Config{ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, @@ -80,17 +76,9 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("mydnsjp: some credentials information are missing") } - client := internal.NewClient(config.MasterID, config.Password) - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, - client: client, + client: internal.NewClient(config.MasterID, config.Password), }, nil } @@ -109,7 +97,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("mydnsjp: %w", err) } - return nil } @@ -122,6 +109,5 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("mydnsjp: %w", err) } - return nil } diff --git a/providers/dns/mydnsjp/mydnsjp.toml b/providers/dns/mydnsjp/mydnsjp.toml index eb9e73acc..2d3b310e2 100644 --- a/providers/dns/mydnsjp/mydnsjp.toml +++ b/providers/dns/mydnsjp/mydnsjp.toml @@ -7,7 +7,7 @@ Since = "v1.2.0" Example = ''' MYDNSJP_MASTER_ID=xxxxx \ MYDNSJP_PASSWORD=xxxxx \ -lego --dns mydnsjp -d '*.example.com' -d example.com run +lego --email you@example.com --dns mydnsjp --domains my.example.org run ''' [Configuration] @@ -15,9 +15,10 @@ lego --dns mydnsjp -d '*.example.com' -d example.com run MYDNSJP_MASTER_ID = "Master ID" MYDNSJP_PASSWORD = "Password" [Configuration.Additional] - MYDNSJP_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - MYDNSJP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - MYDNSJP_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + MYDNSJP_POLLING_INTERVAL = "Time between DNS propagation check" + MYDNSJP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + MYDNSJP_TTL = "The TTL of the TXT record used for the DNS challenge" + MYDNSJP_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.mydns.jp/?MENU=030" diff --git a/providers/dns/mydnsjp/mydnsjp_test.go b/providers/dns/mydnsjp/mydnsjp_test.go index c82bd2264..96eb95865 100644 --- a/providers/dns/mydnsjp/mydnsjp_test.go +++ b/providers/dns/mydnsjp/mydnsjp_test.go @@ -56,7 +56,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -125,7 +124,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -139,7 +137,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/mythicbeasts/internal/client.go b/providers/dns/mythicbeasts/internal/client.go index 82c51dbf3..91fbbaf54 100644 --- a/providers/dns/mythicbeasts/internal/client.go +++ b/providers/dns/mythicbeasts/internal/client.go @@ -35,7 +35,7 @@ type Client struct { } // NewClient Creates a new Client. -func NewClient(username, password string) *Client { +func NewClient(username string, password string) *Client { apiEndpoint, _ := url.Parse(APIBaseURL) authEndpoint, _ := url.Parse(AuthBaseURL) @@ -99,7 +99,6 @@ func (c *Client) createTXTRecord(ctx context.Context, zone, leaf, recordType, va } resp := &createTXTResponse{} - err = c.do(req, resp) if err != nil { return nil, err diff --git a/providers/dns/mythicbeasts/internal/client_test.go b/providers/dns/mythicbeasts/internal/client_test.go index acbf85268..7e3857986 100644 --- a/providers/dns/mythicbeasts/internal/client_test.go +++ b/providers/dns/mythicbeasts/internal/client_test.go @@ -1,54 +1,69 @@ package internal import ( + "fmt" + "io" + "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" "time" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("user", "secret") - client.HTTPClient = server.Client() - client.APIEndpoint, _ = url.Parse(server.URL) - client.token = &Token{ - Token: "secret", - Lifetime: 60, - TokenType: "bearer", - Deadline: time.Now().Add(1 * time.Minute), - } +func setupTest(t *testing.T, pattern string, handler http.HandlerFunc) *Client { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("Bearer "+fakeToken), - ) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc(pattern, handler) + + client := NewClient("user", "secret") + client.HTTPClient = server.Client() + client.APIEndpoint, _ = url.Parse(server.URL) + client.token = &Token{ + Token: "secret", + Lifetime: 60, + TokenType: "bearer", + Deadline: time.Now().Add(1 * time.Minute), + } + + return client +} + +func writeFixtureHandler(method, filename string) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, _ = io.Copy(rw, file) + } } func TestClient_CreateTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /zones/example.com/records/foo/TXT", - servermock.ResponseFromFixture("post-zoneszonerecords.json"), - servermock.CheckRequestJSONBody(`{"records":[{"host":"foo","ttl":120,"type":"TXT","data":"txt"}]}`)). - Build(t) + client := setupTest(t, "/zones/example.com/records/foo/TXT", writeFixtureHandler(http.MethodPost, "post-zoneszonerecords.json")) - err := client.CreateTXTRecord(mockContext(t), "example.com", "foo", "txt", 120) + err := client.CreateTXTRecord(mockContext(), "example.com", "foo", "txt", 120) require.NoError(t, err) } func TestClient_RemoveTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /zones/example.com/records/foo/TXT", - servermock.ResponseFromFixture("delete-zoneszonerecords.json"), - servermock.CheckQueryParameter().Strict(). - With("data", "txt")). - Build(t) + client := setupTest(t, "/zones/example.com/records/foo/TXT", writeFixtureHandler(http.MethodDelete, "delete-zoneszonerecords.json")) - err := client.RemoveTXTRecord(mockContext(t), "example.com", "foo", "txt") + err := client.RemoveTXTRecord(mockContext(), "example.com", "foo", "txt") require.NoError(t, err) } diff --git a/providers/dns/mythicbeasts/internal/fixtures/token.json b/providers/dns/mythicbeasts/internal/fixtures/token.json deleted file mode 100644 index f23fe58ea..000000000 --- a/providers/dns/mythicbeasts/internal/fixtures/token.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "access_token": "xxx", - "expires_in": 666, - "token_type": "bearer" -} diff --git a/providers/dns/mythicbeasts/internal/identity.go b/providers/dns/mythicbeasts/internal/identity.go index 15e35ba69..417f1c759 100644 --- a/providers/dns/mythicbeasts/internal/identity.go +++ b/providers/dns/mythicbeasts/internal/identity.go @@ -44,7 +44,6 @@ func (c *Client) obtainToken(ctx context.Context) (*Token, error) { } tok := Token{} - err = json.Unmarshal(raw, &tok) if err != nil { return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) @@ -84,7 +83,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) errResp := &authResponseError{} - err := json.Unmarshal(raw, errResp) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/mythicbeasts/internal/identity_test.go b/providers/dns/mythicbeasts/internal/identity_test.go index 3e1e8ba4f..9d8daf827 100644 --- a/providers/dns/mythicbeasts/internal/identity_test.go +++ b/providers/dns/mythicbeasts/internal/identity_test.go @@ -2,72 +2,80 @@ package internal import ( "context" + "encoding/json" + "fmt" + "net/http" "net/http/httptest" "net/url" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -const fakeToken = "xxx" - -func mockContext(t *testing.T) context.Context { - t.Helper() - - return context.WithValue(t.Context(), tokenKey, &Token{Token: fakeToken}) +func mockContext() context.Context { + return context.WithValue(context.Background(), tokenKey, &Token{Token: "xxx"}) } -func mockBuilderIdentity() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("user", "secret") - client.HTTPClient = server.Client() - client.AuthEndpoint, _ = url.Parse(server.URL) +func tokenHandler(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("invalid method, got %s want %s", req.Method, http.MethodPost), http.StatusMethodNotAllowed) + return + } - return client, nil - }, - servermock.CheckHeader(). - WithBasicAuth("user", "secret"), - servermock.CheckHeader(). - WithContentTypeFromURLEncoded()) + username, password, ok := req.BasicAuth() + if !ok || username != "user" || password != "secret" { + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + _ = json.NewEncoder(rw).Encode(Token{ + Token: "xxx", + Lifetime: 666, + TokenType: "bearer", + }) } func TestClient_obtainToken(t *testing.T) { - client := mockBuilderIdentity(). - Route("POST /", - servermock.ResponseFromFixture("token.json"), - servermock.CheckForm().Strict(). - With("grant_type", "client_credentials")). - Build(t) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc("/", tokenHandler) + + client := NewClient("user", "secret") + client.HTTPClient = server.Client() + client.AuthEndpoint, _ = url.Parse(server.URL) assert.Nil(t, client.token) - tok, err := client.obtainToken(t.Context()) + tok, err := client.obtainToken(context.Background()) require.NoError(t, err) assert.NotNil(t, tok) assert.NotZero(t, tok.Deadline) - assert.Equal(t, fakeToken, tok.Token) + assert.Equal(t, "xxx", tok.Token) } func TestClient_CreateAuthenticatedContext(t *testing.T) { - client := mockBuilderIdentity(). - Route("POST /", - servermock.ResponseFromFixture("token.json"), - servermock.CheckForm().Strict(). - With("grant_type", "client_credentials")). - Build(t) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc("/", tokenHandler) + + client := NewClient("user", "secret") + client.HTTPClient = server.Client() + client.AuthEndpoint, _ = url.Parse(server.URL) assert.Nil(t, client.token) - ctx, err := client.CreateAuthenticatedContext(t.Context()) + ctx, err := client.CreateAuthenticatedContext(context.Background()) require.NoError(t, err) tok := getToken(ctx) assert.NotNil(t, tok) assert.NotZero(t, tok.Deadline) - assert.Equal(t, fakeToken, tok.Token) + assert.Equal(t, "xxx", tok.Token) } diff --git a/providers/dns/mythicbeasts/mythicbeasts.go b/providers/dns/mythicbeasts/mythicbeasts.go index e8f5081f7..a23ff5701 100644 --- a/providers/dns/mythicbeasts/mythicbeasts.go +++ b/providers/dns/mythicbeasts/mythicbeasts.go @@ -9,10 +9,8 @@ import ( "net/url" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/mythicbeasts/internal" ) @@ -31,8 +29,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { UserName string @@ -88,7 +84,6 @@ func NewDNSProvider() (*DNSProvider, error) { if err != nil { return nil, fmt.Errorf("mythicbeasts: %w", err) } - config.UserName = values[EnvUserName] config.Password = values[EnvPassword] @@ -119,8 +114,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{config: config, client: client}, nil } diff --git a/providers/dns/mythicbeasts/mythicbeasts.toml b/providers/dns/mythicbeasts/mythicbeasts.toml index cada3041d..6cb3a28f0 100644 --- a/providers/dns/mythicbeasts/mythicbeasts.toml +++ b/providers/dns/mythicbeasts/mythicbeasts.toml @@ -7,7 +7,7 @@ Since = "v0.3.7" Example = ''' MYTHICBEASTS_USERNAME=myuser \ MYTHICBEASTS_PASSWORD=mypass \ -lego --dns mythicbeasts -d '*.example.com' -d example.com run +lego --email you@example.com --dns mythicbeasts --domains my.example.org run ''' Additional = ''' @@ -23,10 +23,10 @@ Your API key name is not needed to operate lego. [Configuration.Additional] MYTHICBEASTS_API_ENDPOINT = "The endpoint for the API (must implement v2)" MYTHICBEASTS_AUTH_API_ENDPOINT = "The endpoint for Mythic Beasts' Authentication" - MYTHICBEASTS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - MYTHICBEASTS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - MYTHICBEASTS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - MYTHICBEASTS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 10)" + MYTHICBEASTS_POLLING_INTERVAL = "Time between DNS propagation check" + MYTHICBEASTS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + MYTHICBEASTS_TTL = "The TTL of the TXT record used for the DNS challenge" + MYTHICBEASTS_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.mythic-beasts.com/support/api/dnsv2" diff --git a/providers/dns/mythicbeasts/mythicbeasts_test.go b/providers/dns/mythicbeasts/mythicbeasts_test.go index c684725b7..5a8a9d4bb 100644 --- a/providers/dns/mythicbeasts/mythicbeasts_test.go +++ b/providers/dns/mythicbeasts/mythicbeasts_test.go @@ -57,7 +57,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -109,7 +108,6 @@ func TestNewDNSProviderConfig(t *testing.T) { t.Run(test.desc, func(t *testing.T) { config, err := NewDefaultConfig() require.NoError(t, err) - config.UserName = test.username config.Password = test.password @@ -132,7 +130,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -146,7 +143,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/namecheap/internal/client.go b/providers/dns/namecheap/internal/client.go index 6fb737b95..f7ca8f66f 100644 --- a/providers/dns/namecheap/internal/client.go +++ b/providers/dns/namecheap/internal/client.go @@ -32,7 +32,7 @@ type Client struct { } // NewClient creates a new Client. -func NewClient(apiUser, apiKey, clientIP string) *Client { +func NewClient(apiUser string, apiKey string, clientIP string) *Client { return &Client{ apiUser: apiUser, apiKey: apiKey, @@ -54,7 +54,6 @@ func (c *Client) GetHosts(ctx context.Context, sld, tld string) ([]Record, error } var ghr getHostsResponse - err = c.do(request, &ghr) if err != nil { return nil, err @@ -89,7 +88,6 @@ func (c *Client) SetHosts(ctx context.Context, sld, tld string, hosts []Record) } var shr setHostsResponse - err = c.do(req, &shr) if err != nil { return err @@ -98,7 +96,6 @@ func (c *Client) SetHosts(ctx context.Context, sld, tld string, hosts []Record) if len(shr.Errors) > 0 { return shr.Errors[0] } - if shr.Result.IsSuccess != "true" { return errors.New("setHosts failed") } diff --git a/providers/dns/namecheap/internal/client_test.go b/providers/dns/namecheap/internal/client_test.go index d7bea7b6e..9d78ee213 100644 --- a/providers/dns/namecheap/internal/client_test.go +++ b/providers/dns/namecheap/internal/client_test.go @@ -1,38 +1,75 @@ package internal import ( + "context" + "fmt" + "io" + "net/http" "net/http/httptest" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func setupClient(server *httptest.Server) (*Client, error) { +func setupTest(t *testing.T, handler http.HandlerFunc) *Client { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc("/", handler) + client := NewClient("user", "secret", "127.0.0.1") client.HTTPClient = server.Client() client.BaseURL = server.URL - return client, nil + return client +} + +func writeFixture(rw http.ResponseWriter, filename string) { + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, _ = io.Copy(rw, file) } func TestClient_GetHosts(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /", - servermock.ResponseFromFixture("getHosts.xml"), - servermock.CheckQueryParameter().Strict(). - With("ApiKey", "secret"). - With("ApiUser", "user"). - With("ClientIp", "127.0.0.1"). - With("Command", "namecheap.domains.dns.getHosts"). - With("SLD", "foo"). - With("TLD", "example.com"). - With("UserName", "user"), - ). - Build(t) + client := setupTest(t, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } - hosts, err := client.GetHosts(t.Context(), "foo", "example.com") + expectedParams := map[string]string{ + "ApiKey": "secret", + "ApiUser": "user", + "ClientIp": "127.0.0.1", + "Command": "namecheap.domains.dns.getHosts", + "SLD": "foo", + "TLD": "example.com", + "UserName": "user", + } + + query := req.URL.Query() + for k, v := range expectedParams { + if query.Get(k) != v { + http.Error(rw, fmt.Sprintf("invalid query parameter %s value: %s", k, query.Get(k)), http.StatusBadRequest) + return + } + } + + writeFixture(rw, "getHosts.xml") + }) + + hosts, err := client.GetHosts(context.Background(), "foo", "example.com") require.NoError(t, err) expected := []Record{ @@ -44,62 +81,93 @@ func TestClient_GetHosts(t *testing.T) { } func TestClient_GetHosts_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /", - servermock.ResponseFromFixture("getHosts_errorBadAPIKey1.xml")). - Build(t) + client := setupTest(t, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } - _, err := client.GetHosts(t.Context(), "foo", "example.com") + writeFixture(rw, "getHosts_errorBadAPIKey1.xml") + }) + + _, err := client.GetHosts(context.Background(), "foo", "example.com") require.ErrorAs(t, err, &apiError{}) } func TestClient_SetHosts(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient, servermock.CheckHeader().WithContentTypeFromURLEncoded()). - Route("POST /", - servermock.ResponseFromFixture("setHosts.xml"), - servermock.CheckForm().Strict(). - With("ApiKey", "secret"). - With("ApiUser", "user"). - With("ClientIp", "127.0.0.1"). - With("Command", "namecheap.domains.dns.setHosts"). - With("SLD", "foo"). - With("TLD", "example.com"). - With("UserName", "user"). - // entry 1 - With("HostName1", "_acme-challenge.test.example.com"). - With("RecordType1", "TXT"). - With("Address1", "txtTXTtxt"). - With("MXPref1", "10"). - With("TTL1", "120"). - // entry 2 - With("HostName2", "_acme-challenge.test.example.org"). - With("RecordType2", "TXT"). - With("Address2", "txtTXTtxt"). - With("MXPref2", "10"). - With("TTL2", "120"), - ). - Build(t) + client := setupTest(t, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + if req.Header.Get("Content-Type") != "application/x-www-form-urlencoded" { + http.Error(rw, fmt.Sprintf("invalid Content-Type: %s", req.Header.Get("Content-Type")), http.StatusBadRequest) + return + } + + err := req.ParseForm() + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + expectedParams := map[string]string{ + "HostName1": "_acme-challenge.test.example.com", + "RecordType1": "TXT", + "Address1": "txtTXTtxt", + "MXPref1": "10", + "TTL1": "120", + + "HostName2": "_acme-challenge.test.example.org", + "RecordType2": "TXT", + "Address2": "txtTXTtxt", + "MXPref2": "10", + "TTL2": "120", + + "ApiKey": "secret", + "ApiUser": "user", + "ClientIp": "127.0.0.1", + "Command": "namecheap.domains.dns.setHosts", + "SLD": "foo", + "TLD": "example.com", + "UserName": "user", + } + + for k, v := range expectedParams { + if req.Form.Get(k) != v { + http.Error(rw, fmt.Sprintf("invalid form data %s value: %q", k, req.Form.Get(k)), http.StatusBadRequest) + return + } + } + + writeFixture(rw, "setHosts.xml") + }) records := []Record{ {Name: "_acme-challenge.test.example.com", Type: "TXT", Address: "txtTXTtxt", MXPref: "10", TTL: "120"}, {Name: "_acme-challenge.test.example.org", Type: "TXT", Address: "txtTXTtxt", MXPref: "10", TTL: "120"}, } - err := client.SetHosts(t.Context(), "foo", "example.com", records) + err := client.SetHosts(context.Background(), "foo", "example.com", records) require.NoError(t, err) } func TestClient_SetHosts_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("POST /", - servermock.ResponseFromFixture("setHosts_errorBadAPIKey1.xml")). - Build(t) + client := setupTest(t, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + writeFixture(rw, "setHosts_errorBadAPIKey1.xml") + }) records := []Record{ {Name: "_acme-challenge.test.example.com", Type: "TXT", Address: "txtTXTtxt", MXPref: "10", TTL: "120"}, {Name: "_acme-challenge.test.example.org", Type: "TXT", Address: "txtTXTtxt", MXPref: "10", TTL: "120"}, } - err := client.SetHosts(t.Context(), "foo", "example.com", records) + err := client.SetHosts(context.Background(), "foo", "example.com", records) require.ErrorAs(t, err, &apiError{}) } diff --git a/providers/dns/namecheap/namecheap.go b/providers/dns/namecheap/namecheap.go index 54640f8e0..c4d9c0699 100644 --- a/providers/dns/namecheap/namecheap.go +++ b/providers/dns/namecheap/namecheap.go @@ -10,11 +10,9 @@ import ( "strings" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/namecheap/internal" "golang.org/x/net/publicsuffix" ) @@ -47,7 +45,47 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) +// A challenge represents all the data needed to specify a dns-01 challenge to lets-encrypt. +type challenge struct { + domain string + key string + keyFqdn string + keyValue string + tld string + sld string + host string +} + +// newChallenge builds a challenge record from a domain name and a challenge authentication key. +func newChallenge(domain, keyAuth string) (*challenge, error) { + domain = dns01.UnFqdn(domain) + + tld, _ := publicsuffix.PublicSuffix(domain) + if tld == domain { + return nil, fmt.Errorf("invalid domain name %q", domain) + } + + parts := strings.Split(domain, ".") + longest := len(parts) - strings.Count(tld, ".") - 1 + sld := parts[longest-1] + + var host string + if longest >= 1 { + host = strings.Join(parts[:longest-1], ".") + } + + info := dns01.GetChallengeInfo(domain, keyAuth) + + return &challenge{ + domain: domain, + key: "_acme-challenge." + host, + keyFqdn: info.EffectiveFQDN, + keyValue: info.Value, + tld: tld, + sld: sld, + host: host, + }, nil +} // Config is used to configure the creation of the DNSProvider. type Config struct { @@ -73,11 +111,10 @@ func NewDefaultConfig() *Config { BaseURL: baseURL, Debug: env.GetOrDefaultBool(EnvDebug, false), TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, time.Hour), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 60*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 15*time.Second), HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, time.Minute), - Transport: defaultTransport(envNamespace), + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 60*time.Second), }, } } @@ -119,7 +156,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if err != nil { return nil, fmt.Errorf("namecheap: %w", err) } - config.ClientIP = clientIP } @@ -130,8 +166,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{config: config, client: client}, nil } @@ -144,22 +178,22 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present installs a TXT record for the DNS challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { // TODO(ldez) replace domain by FQDN to follow CNAME. - pr, err := newPseudoRecord(domain, keyAuth) + ch, err := newChallenge(domain, keyAuth) if err != nil { return fmt.Errorf("namecheap: %w", err) } ctx := context.Background() - records, err := d.client.GetHosts(ctx, pr.sld, pr.tld) + records, err := d.client.GetHosts(ctx, ch.sld, ch.tld) if err != nil { return fmt.Errorf("namecheap: %w", err) } record := internal.Record{ - Name: pr.key, + Name: ch.key, Type: "TXT", - Address: pr.keyValue, + Address: ch.keyValue, MXPref: "10", TTL: strconv.Itoa(d.config.TTL), } @@ -172,37 +206,33 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { } } - err = d.client.SetHosts(ctx, pr.sld, pr.tld, records) + err = d.client.SetHosts(ctx, ch.sld, ch.tld, records) if err != nil { return fmt.Errorf("namecheap: %w", err) } - return nil } // CleanUp removes a TXT record used for a previous DNS challenge. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { // TODO(ldez) replace domain by FQDN to follow CNAME. - pr, err := newPseudoRecord(domain, keyAuth) + ch, err := newChallenge(domain, keyAuth) if err != nil { return fmt.Errorf("namecheap: %w", err) } ctx := context.Background() - records, err := d.client.GetHosts(ctx, pr.sld, pr.tld) + records, err := d.client.GetHosts(ctx, ch.sld, ch.tld) if err != nil { return fmt.Errorf("namecheap: %w", err) } // Find the challenge TXT record and remove it if found. - var ( - found bool - newRecords []internal.Record - ) - + var found bool + var newRecords []internal.Record for _, h := range records { - if h.Name == pr.key && h.Type == "TXT" { + if h.Name == ch.key && h.Type == "TXT" { found = true } else { newRecords = append(newRecords, h) @@ -213,52 +243,9 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return nil } - err = d.client.SetHosts(ctx, pr.sld, pr.tld, newRecords) + err = d.client.SetHosts(ctx, ch.sld, ch.tld, newRecords) if err != nil { return fmt.Errorf("namecheap: %w", err) } - return nil } - -// A pseudoRecord represents all the data needed to specify a dns-01 challenge to lets-encrypt. -type pseudoRecord struct { - domain string - key string - keyFqdn string - keyValue string - tld string - sld string - host string -} - -// newPseudoRecord builds a challenge record from a domain name and a challenge authentication key. -func newPseudoRecord(domain, keyAuth string) (*pseudoRecord, error) { - domain = dns01.UnFqdn(domain) - - tld, _ := publicsuffix.PublicSuffix(domain) - if tld == domain { - return nil, fmt.Errorf("invalid domain name %q", domain) - } - - parts := strings.Split(domain, ".") - longest := len(parts) - strings.Count(tld, ".") - 1 - sld := parts[longest-1] - - var host string - if longest >= 1 { - host = strings.Join(parts[:longest-1], ".") - } - - info := dns01.GetChallengeInfo(domain, keyAuth) - - return &pseudoRecord{ - domain: domain, - key: "_acme-challenge." + host, - keyFqdn: info.EffectiveFQDN, - keyValue: info.Value, - tld: tld, - sld: sld, - host: host, - }, nil -} diff --git a/providers/dns/namecheap/namecheap.toml b/providers/dns/namecheap/namecheap.toml index b0f92a1bd..004b2a4a1 100644 --- a/providers/dns/namecheap/namecheap.toml +++ b/providers/dns/namecheap/namecheap.toml @@ -14,7 +14,7 @@ More information in the section [Enabling API Access](https://www.namecheap.com/ Example = ''' NAMECHEAP_API_USER=user \ NAMECHEAP_API_KEY=key \ -lego --dns namecheap -d '*.example.com' -d example.com run +lego --email you@example.com --dns namecheap --domains my.example.org run ''' [Configuration] @@ -22,10 +22,10 @@ lego --dns namecheap -d '*.example.com' -d example.com run NAMECHEAP_API_USER = "API user" NAMECHEAP_API_KEY = "API key" [Configuration.Additional] - NAMECHEAP_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 15)" - NAMECHEAP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 3600)" - NAMECHEAP_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - NAMECHEAP_HTTP_TIMEOUT = "API request timeout in seconds (Default: 60)" + NAMECHEAP_POLLING_INTERVAL = "Time between DNS propagation check" + NAMECHEAP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + NAMECHEAP_TTL = "The TTL of the TXT record used for the DNS challenge" + NAMECHEAP_HTTP_TIMEOUT = "API request timeout" NAMECHEAP_SANDBOX = "Activate the sandbox (boolean)" [Links] diff --git a/providers/dns/namecheap/namecheap_test.go b/providers/dns/namecheap/namecheap_test.go index e55a4a6bc..4f4036ded 100644 --- a/providers/dns/namecheap/namecheap_test.go +++ b/providers/dns/namecheap/namecheap_test.go @@ -1,10 +1,16 @@ package namecheap import ( + "io" + "net/http" "net/http/httptest" + "net/url" + "os" + "path/filepath" "testing" + "time" - "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/go-acme/lego/v4/providers/dns/namecheap/internal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -18,6 +24,7 @@ const ( type testCase struct { name string domain string + hosts []internal.Record errString string getHostsResponse string setHostsResponse string @@ -25,14 +32,26 @@ type testCase struct { var testCases = []testCase{ { - name: "Test:Success:1", - domain: "test.example.com", + name: "Test:Success:1", + domain: "test.example.com", + hosts: []internal.Record{ + {Type: "A", Name: "home", Address: "10.0.0.1", MXPref: "10", TTL: "1799"}, + {Type: "A", Name: "www", Address: "10.0.0.2", MXPref: "10", TTL: "1200"}, + {Type: "AAAA", Name: "a", Address: "::0", MXPref: "10", TTL: "1799"}, + {Type: "CNAME", Name: "*", Address: "example.com.", MXPref: "10", TTL: "1799"}, + {Type: "MXE", Name: "example.com", Address: "10.0.0.5", MXPref: "10", TTL: "1800"}, + {Type: "URL", Name: "xyz", Address: "https://google.com", MXPref: "10", TTL: "1799"}, + }, getHostsResponse: "getHosts_success1.xml", setHostsResponse: "setHosts_success1.xml", }, { - name: "Test:Success:2", - domain: "example.com", + name: "Test:Success:2", + domain: "example.com", + hosts: []internal.Record{ + {Type: "A", Name: "@", Address: "10.0.0.2", MXPref: "10", TTL: "1200"}, + {Type: "A", Name: "www", Address: "10.0.0.3", MXPref: "10", TTL: "60"}, + }, getHostsResponse: "getHosts_success2.xml", setHostsResponse: "setHosts_success2.xml", }, @@ -44,37 +63,96 @@ var testCases = []testCase{ }, } +func setupTest(t *testing.T, tc *testCase) *DNSProvider { + t.Helper() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + values := r.URL.Query() + cmd := values.Get("Command") + switch cmd { + case "namecheap.domains.dns.getHosts": + assertHdr(t, tc, &values) + w.WriteHeader(http.StatusOK) + writeFixture(w, tc.getHostsResponse) + default: + t.Errorf("Unexpected GET command: %s", cmd) + } + + case http.MethodPost: + err := r.ParseForm() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + values := r.Form + cmd := values.Get("Command") + switch cmd { + case "namecheap.domains.dns.setHosts": + assertHdr(t, tc, &values) + w.WriteHeader(http.StatusOK) + writeFixture(w, tc.setHostsResponse) + default: + t.Errorf("Unexpected POST command: %s", cmd) + } + + default: + t.Errorf("Unexpected http method: %s", r.Method) + } + }) + + server := httptest.NewServer(handler) + t.Cleanup(server.Close) + + return mockDNSProvider(t, server.URL) +} + +func mockDNSProvider(t *testing.T, baseURL string) *DNSProvider { + t.Helper() + + config := NewDefaultConfig() + config.BaseURL = baseURL + config.APIUser = envTestUser + config.APIKey = envTestKey + config.ClientIP = envTestClientIP + config.HTTPClient = &http.Client{Timeout: 60 * time.Second} + + provider, err := NewDNSProviderConfig(config) + require.NoError(t, err) + + return provider +} + +func assertHdr(t *testing.T, tc *testCase, values *url.Values) { + t.Helper() + + ch, _ := newChallenge(tc.domain, "") + assert.Equal(t, envTestUser, values.Get("ApiUser"), "ApiUser") + assert.Equal(t, envTestKey, values.Get("ApiKey"), "ApiKey") + assert.Equal(t, envTestUser, values.Get("UserName"), "UserName") + assert.Equal(t, envTestClientIP, values.Get("ClientIp"), "ClientIp") + assert.Equal(t, ch.sld, values.Get("SLD"), "SLD") + assert.Equal(t, ch.tld, values.Get("TLD"), "TLD") +} + +func writeFixture(rw http.ResponseWriter, filename string) { + file, err := os.Open(filepath.Join("internal", "fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, _ = io.Copy(rw, file) +} + func TestDNSProvider_Present(t *testing.T) { for _, test := range testCases { t.Run(test.name, func(t *testing.T) { - ch, _ := newPseudoRecord(test.domain, "") + p := setupTest(t, &test) - provider := mockBuilder(). - Route("GET /", - servermock.ResponseFromInternal(test.getHostsResponse), - servermock.CheckForm().Strict(). - With("ClientIp", "10.0.0.1"). - With("Command", "namecheap.domains.dns.getHosts"). - With("SLD", ch.sld). - With("TLD", ch.tld). - With("UserName", "foo"). - With("ApiKey", "bar"). - With("ApiUser", "foo"), - ). - Route("POST /", - servermock.ResponseFromInternal(test.setHostsResponse), - servermock.CheckForm(). - With("ClientIp", "10.0.0.1"). - With("Command", "namecheap.domains.dns.setHosts"). - With("SLD", ch.sld). - With("TLD", ch.tld). - With("UserName", "foo"). - With("ApiKey", "bar"). - With("ApiUser", "foo"), - ). - Build(t) - - err := provider.Present(test.domain, "", "dummyKey") + err := p.Present(test.domain, "", "dummyKey") if test.errString != "" { assert.EqualError(t, err, "namecheap: "+test.errString) } else { @@ -87,34 +165,9 @@ func TestDNSProvider_Present(t *testing.T) { func TestDNSProvider_CleanUp(t *testing.T) { for _, test := range testCases { t.Run(test.name, func(t *testing.T) { - ch, _ := newPseudoRecord(test.domain, "") + p := setupTest(t, &test) - provider := mockBuilder(). - Route("GET /", - servermock.ResponseFromInternal(test.getHostsResponse), - servermock.CheckForm().Strict(). - With("ClientIp", "10.0.0.1"). - With("Command", "namecheap.domains.dns.getHosts"). - With("SLD", ch.sld). - With("TLD", ch.tld). - With("UserName", "foo"). - With("ApiKey", "bar"). - With("ApiUser", "foo"), - ). - Route("POST /", - servermock.ResponseFromInternal(test.setHostsResponse), - servermock.CheckForm(). - With("ClientIp", "10.0.0.1"). - With("Command", "namecheap.domains.dns.setHosts"). - With("SLD", ch.sld). - With("TLD", ch.tld). - With("UserName", "foo"). - With("ApiKey", "bar"). - With("ApiUser", "foo"), - ). - Build(t) - - err := provider.CleanUp(test.domain, "", "dummyKey") + err := p.CleanUp(test.domain, "", "dummyKey") if test.errString != "" { assert.EqualError(t, err, "namecheap: "+test.errString) } else { @@ -124,7 +177,7 @@ func TestDNSProvider_CleanUp(t *testing.T) { } } -func Test_newPseudoRecord_domainSplit(t *testing.T) { +func TestDomainSplit(t *testing.T) { tests := []struct { domain string valid bool @@ -152,8 +205,7 @@ func Test_newPseudoRecord_domainSplit(t *testing.T) { for _, test := range tests { t.Run(test.domain, func(t *testing.T) { valid := true - - ch, err := newPseudoRecord(test.domain, "") + ch, err := newChallenge(test.domain, "") if err != nil { valid = false } @@ -174,16 +226,3 @@ func Test_newPseudoRecord_domainSplit(t *testing.T) { }) } } - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.HTTPClient = server.Client() - config.BaseURL = server.URL - config.APIUser = envTestUser - config.APIKey = envTestKey - config.ClientIP = envTestClientIP - - return NewDNSProviderConfig(config) - }) -} diff --git a/providers/dns/namecheap/transport.go b/providers/dns/namecheap/transport.go deleted file mode 100644 index 584dc6e50..000000000 --- a/providers/dns/namecheap/transport.go +++ /dev/null @@ -1,71 +0,0 @@ -package namecheap - -import ( - "net/http" - "net/url" - "strings" - "sync" - - "github.com/go-acme/lego/v4/platform/config/env" - "golang.org/x/net/http/httpproxy" -) - -const ( - envHTTPProxy = "HTTP_PROXY" - envHTTPProxyLower = "http_proxy" - envHTTPSProxy = "HTTPS_PROXY" - envHTTPSProxyLower = "https_proxy" - envNoProxy = "NO_PROXY" - envNoProxyLower = "no_proxy" - envRequestMethod = "REQUEST_METHOD" -) - -// Allows lazy loading of the proxy. -var ( - envProxyOnce sync.Once - envProxyFuncValue func(*url.URL) (*url.URL, error) -) - -func defaultTransport(namespace string) http.RoundTripper { - tr, ok := http.DefaultTransport.(*http.Transport) - if !ok { - return nil - } - - clone := tr.Clone() - clone.Proxy = proxyFromEnvironment(namespace) - - return clone -} - -// Inspired by: -// - https://pkg.go.dev/net/http#ProxyFromEnvironment -// - https://pkg.go.dev/golang.org/x/net/http/httpproxy#FromEnvironment -func envProxyFunc(namespace string) func(*url.URL) (*url.URL, error) { - envProxyOnce.Do(func() { - cfg := &httpproxy.Config{ - HTTPProxy: getEnv(namespace, envHTTPProxy, envHTTPProxyLower), - HTTPSProxy: getEnv(namespace, envHTTPSProxy, envHTTPSProxyLower), - NoProxy: getEnv(namespace, envNoProxy, envNoProxyLower), - CGI: env.GetOneWithFallback(namespace+envRequestMethod, "", env.ParseString, envRequestMethod) != "", - } - - envProxyFuncValue = cfg.ProxyFunc() - }) - - return envProxyFuncValue -} - -// Inspired by: -// - https://pkg.go.dev/net/http#ProxyFromEnvironment -// - https://pkg.go.dev/golang.org/x/net/http/httpproxy#FromEnvironment -func proxyFromEnvironment(namespace string) func(req *http.Request) (*url.URL, error) { - return func(req *http.Request) (*url.URL, error) { - return envProxyFunc(namespace)(req.URL) - } -} - -func getEnv(namespace, baseEnvName, baseEnvNameLower string) string { - return env.GetOneWithFallback(namespace+baseEnvName, "", env.ParseString, - strings.ToLower(namespace)+baseEnvNameLower, baseEnvName, baseEnvNameLower) -} diff --git a/providers/dns/namecheap/transport_test.go b/providers/dns/namecheap/transport_test.go deleted file mode 100644 index cd3e9ff17..000000000 --- a/providers/dns/namecheap/transport_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package namecheap - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func Test_defaultTransport(t *testing.T) { - client := servermock.NewBuilder( - func(server *httptest.Server) (*http.Client, error) { - cl := server.Client() - - t.Setenv("NAMECHEAP_HTTP_PROXY", server.URL) - - cl.Transport = defaultTransport(envNamespace) - - return cl, nil - }). - Route("/", - servermock.Noop().WithStatusCode(http.StatusTeapot)). - Build(t) - - req, err := http.NewRequest(http.MethodGet, "http://example.com", nil) - require.NoError(t, err) - - resp, err := client.Do(req) - require.NoError(t, err) - - t.Cleanup(func() { - _ = resp.Body.Close() - }) - - assert.Equal(t, http.StatusTeapot, resp.StatusCode) -} diff --git a/providers/dns/namedotcom/namedotcom.go b/providers/dns/namedotcom/namedotcom.go index 04c8b5967..e49a15a9a 100644 --- a/providers/dns/namedotcom/namedotcom.go +++ b/providers/dns/namedotcom/namedotcom.go @@ -7,13 +7,14 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/namedotcom/go/v4/namecom" + "github.com/namedotcom/go/namecom" ) +// according to https://www.name.com/api-docs/DNS#CreateRecord +const minTTL = 300 + // Environment variables names. const ( envNamespace = "NAMECOM_" @@ -28,11 +29,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -// according to https://www.name.com/api-docs/DNS#CreateRecord -const minTTL = 300 - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Username string @@ -98,12 +94,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { } client := namecom.New(config.Username, config.APIToken) - - if config.HTTPClient != nil { - client.Client = config.HTTPClient - } - - client.Client = clientdebug.Wrap(client.Client) + client.Client = config.HTTPClient if config.Server != "" { client.Server = config.Server @@ -116,10 +107,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) - if info.EffectiveFQDN != info.FQDN { - domain = dns01.UnFqdn(info.EffectiveFQDN) - } - + // TODO(ldez) replace domain by FQDN to follow CNAME. domainDetails, err := d.client.GetDomain(&namecom.GetDomainRequest{DomainName: domain}) if err != nil { return fmt.Errorf("namedotcom: API call failed: %w", err) @@ -130,6 +118,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("namedotcom: %w", err) } + // TODO(ldez) replace domain by FQDN to follow CNAME. request := &namecom.Record{ DomainName: domain, Host: subDomain, @@ -150,10 +139,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) - if info.EffectiveFQDN != info.FQDN { - domain = dns01.UnFqdn(info.EffectiveFQDN) - } - + // TODO(ldez) replace domain by FQDN to follow CNAME. records, err := d.getRecords(domain) if err != nil { return fmt.Errorf("namedotcom: %w", err) @@ -161,11 +147,11 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { for _, rec := range records { if rec.Fqdn == info.EffectiveFQDN && rec.Type == "TXT" { + // TODO(ldez) replace domain by FQDN to follow CNAME. request := &namecom.DeleteRecordRequest{ DomainName: domain, ID: rec.ID, } - _, err := d.client.DeleteRecord(request) if err != nil { return fmt.Errorf("namedotcom: %w", err) @@ -189,7 +175,6 @@ func (d *DNSProvider) getRecords(domain string) ([]*namecom.Record, error) { } var records []*namecom.Record - for request.Page > 0 { response, err := d.client.ListRecords(request) if err != nil { diff --git a/providers/dns/namedotcom/namedotcom.toml b/providers/dns/namedotcom/namedotcom.toml index 3651c424b..41ed103db 100644 --- a/providers/dns/namedotcom/namedotcom.toml +++ b/providers/dns/namedotcom/namedotcom.toml @@ -7,7 +7,7 @@ Since = "v0.5.0" Example = ''' NAMECOM_USERNAME=foo.bar \ NAMECOM_API_TOKEN=a379a6f6eeafb9a55e378c118034e2751e682fab \ -lego --dns namedotcom -d '*.example.com' -d example.com run +lego --email you@example.com --dns namedotcom --domains my.example.org run ''' [Configuration] @@ -15,10 +15,10 @@ lego --dns namedotcom -d '*.example.com' -d example.com run NAMECOM_USERNAME = "Username" NAMECOM_API_TOKEN = "API token" [Configuration.Additional] - NAMECOM_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 20)" - NAMECOM_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 900)" - NAMECOM_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - NAMECOM_HTTP_TIMEOUT = "API request timeout in seconds (Default: 10)" + NAMECOM_POLLING_INTERVAL = "Time between DNS propagation check" + NAMECOM_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + NAMECOM_TTL = "The TTL of the TXT record used for the DNS challenge" + NAMECOM_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.name.com/api-docs/DNS" diff --git a/providers/dns/namedotcom/namedotcom_test.go b/providers/dns/namedotcom/namedotcom_test.go index da9878bdc..c7d4deaa1 100644 --- a/providers/dns/namedotcom/namedotcom_test.go +++ b/providers/dns/namedotcom/namedotcom_test.go @@ -57,7 +57,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -132,7 +131,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -146,7 +144,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/namesilo/namesilo.go b/providers/dns/namesilo/namesilo.go index 0297b4e1c..61d9e6819 100644 --- a/providers/dns/namesilo/namesilo.go +++ b/providers/dns/namesilo/namesilo.go @@ -2,18 +2,20 @@ package namesilo import ( - "context" "errors" "fmt" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/nrdcg/namesilo" ) +const ( + defaultTTL = 3600 + maxTTL = 2592000 +) + // Environment variables names. const ( envNamespace = "NAMESILO_" @@ -25,13 +27,6 @@ const ( EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) -const ( - defaultTTL = 3600 - maxTTL = 2592000 -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string @@ -81,15 +76,12 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, fmt.Errorf("namesilo: TTL should be in [%d, %d]", defaultTTL, maxTTL) } - if config.APIKey == "" { - return nil, errors.New("namesilo: credentials missing") + transport, err := namesilo.NewTokenTransport(config.APIKey) + if err != nil { + return nil, fmt.Errorf("namesilo: %w", err) } - client := namesilo.NewClient(config.APIKey) - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{client: client, config: config}, nil + return &DNSProvider{client: namesilo.NewClient(transport.Client()), config: config}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. @@ -108,7 +100,12 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("namesilo: %w", err) } - _, err = d.client.DnsAddRecord(context.Background(), &namesilo.DnsAddRecordParams{ + err = d.CleanUp(domain, token, keyAuth) + if err != nil { + return err + } + + _, err = d.client.DnsAddRecord(&namesilo.DnsAddRecordParams{ Domain: zoneName, Type: "TXT", Host: subdomain, @@ -118,14 +115,11 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("namesilo: failed to add record %w", err) } - return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) @@ -135,7 +129,7 @@ func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { zoneName := dns01.UnFqdn(zone) - resp, err := d.client.DnsListRecords(ctx, &namesilo.DnsListRecordsParams{Domain: zoneName}) + resp, err := d.client.DnsListRecords(&namesilo.DnsListRecordsParams{Domain: zoneName}) if err != nil { return fmt.Errorf("namesilo: %w", err) } @@ -145,18 +139,16 @@ func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { return fmt.Errorf("namesilo: %w", err) } + var lastErr error for _, r := range resp.Reply.ResourceRecord { - if r.Type == "TXT" && r.Value == info.Value && (r.Host == subdomain || r.Host == dns01.UnFqdn(info.EffectiveFQDN)) { - _, err := d.client.DnsDeleteRecord(ctx, &namesilo.DnsDeleteRecordParams{Domain: zoneName, ID: r.RecordID}) + if r.Type == "TXT" && (r.Host == subdomain || r.Host == dns01.UnFqdn(info.EffectiveFQDN)) { + _, err := d.client.DnsDeleteRecord(&namesilo.DnsDeleteRecordParams{Domain: zoneName, ID: r.RecordID}) if err != nil { - return fmt.Errorf("namesilo: %w", err) + lastErr = fmt.Errorf("namesilo: %w", err) } - - return nil } } - - return fmt.Errorf("namesilo: no TXT record to delete for %s (%s)", info.EffectiveFQDN, info.Value) + return lastErr } // Timeout returns the timeout and interval to use when checking for DNS propagation. diff --git a/providers/dns/namesilo/namesilo.toml b/providers/dns/namesilo/namesilo.toml index 113ddb5c5..a4e8687b1 100644 --- a/providers/dns/namesilo/namesilo.toml +++ b/providers/dns/namesilo/namesilo.toml @@ -6,16 +6,16 @@ Since = "v2.7.0" Example = ''' NAMESILO_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \ -lego --dns namesilo -d '*.example.com' -d example.com run +lego --email you@example.com --dns namesilo --domains my.example.org run ''' [Configuration] [Configuration.Credentials] NAMESILO_API_KEY = "Client ID" [Configuration.Additional] - NAMESILO_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - NAMESILO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60), it is better to set larger than 15 minutes" - NAMESILO_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600), should be in [3600, 2592000]" + NAMESILO_POLLING_INTERVAL = "Time between DNS propagation check" + NAMESILO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation, it is better to set larger than 15m" + NAMESILO_TTL = "The TTL of the TXT record used for the DNS challenge, should be in [3600, 2592000]" [Links] API = "https://www.namesilo.com/api_reference.php" diff --git a/providers/dns/namesilo/namesilo_test.go b/providers/dns/namesilo/namesilo_test.go index 09eacd035..4b01d7388 100644 --- a/providers/dns/namesilo/namesilo_test.go +++ b/providers/dns/namesilo/namesilo_test.go @@ -45,7 +45,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -78,7 +77,7 @@ func TestNewDNSProviderConfig(t *testing.T) { { desc: "missing API key", ttl: defaultTTL, - expected: "namesilo: credentials missing", + expected: "namesilo: credentials missing: API key", }, { desc: "unavailable TTL", @@ -113,7 +112,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/namesurfer/internal/client.go b/providers/dns/namesurfer/internal/client.go deleted file mode 100644 index e40a7988c..000000000 --- a/providers/dns/namesurfer/internal/client.go +++ /dev/null @@ -1,226 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "crypto/hmac" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "slices" - "strconv" - "strings" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" -) - -type Client struct { - apiKey string - apiSecret string - - BaseURL *url.URL - HTTPClient *http.Client -} - -func NewClient(baseURL, apiKey, apiSecret string) (*Client, error) { - if apiKey == "" || apiSecret == "" { - return nil, errors.New("credentials missing") - } - - if baseURL == "" { - return nil, errors.New("base URL missing") - } - - apiEndpoint, err := url.Parse(baseURL) - if err != nil { - return nil, err - } - - return &Client{ - apiKey: apiKey, - apiSecret: apiSecret, - BaseURL: apiEndpoint.JoinPath("jsonrpc10"), - HTTPClient: &http.Client{ - Timeout: 5 * time.Second, - }, - }, nil -} - -// AddDNSRecord adds a DNS record. -// http://95.128.3.201:8053/API/NSService_10#addDNSRecord -func (d *Client) AddDNSRecord(ctx context.Context, zoneName, viewName string, record DNSNode) error { - digest := d.computeDigest( - zoneName, - viewName, - record.Name, - record.Type, - strconv.Itoa(record.TTL), - record.Data, - ) - - // JSON-RPC 1.0 requires positional parameters array - params := []any{ - digest, - zoneName, - viewName, - record, - } - - var ok bool - - err := d.doRequest(ctx, "addDNSRecord", params, &ok) - if err != nil { - return err - } - - if !ok { - return errors.New("addDNSRecord failed") - } - - return nil -} - -// UpdateDNSHost updates a DNS host record. -// Passing an empty newNode removes the oldNode. -// http://95.128.3.201:8053/API/NSService_10#updateDNSHost -func (d *Client) UpdateDNSHost(ctx context.Context, zoneName, viewName string, oldNode, newNode DNSNode) error { - digest := d.computeDigest(zoneName, viewName) - - // JSON-RPC 1.0 requires positional parameters array - params := []any{ - digest, - zoneName, - viewName, - oldNode, - newNode, - } - - var ok bool - - err := d.doRequest(ctx, "updateDNSHost", params, &ok) - if err != nil { - return err - } - - if !ok { - return errors.New("updateDNSHost failed") - } - - return nil -} - -// SearchDNSHosts searches for DNS host records. -// http://95.128.3.201:8053/API/NSService_10#searchDNSHosts -func (d *Client) SearchDNSHosts(ctx context.Context, pattern string) ([]DNSNode, error) { - digest := d.computeDigest(pattern) - - // JSON-RPC 1.0 requires positional parameters array - params := []any{ - digest, - pattern, - } - - var nodes []DNSNode - - err := d.doRequest(ctx, "searchDNSHosts", params, &nodes) - if err != nil { - return nil, err - } - - return nodes, nil -} - -// ListZones lists DNS zones. -// http://95.128.3.201:8053/API/NSService_10#listZones -func (d *Client) ListZones(ctx context.Context, mode string) ([]DNSZone, error) { - digest := d.computeDigest() - - // JSON-RPC 1.0 requires positional parameters array - params := []any{ - digest, - mode, - } - - var zones []DNSZone - - err := d.doRequest(ctx, "listZones", params, &zones) - if err != nil { - return nil, err - } - - return zones, nil -} - -func (d *Client) doRequest(ctx context.Context, method string, params []any, result any) error { - payload := APIRequest{ - ID: 1, - Method: method, - Params: slices.Concat([]any{d.apiKey}, params), - } - - buf := new(bytes.Buffer) - - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return fmt.Errorf("failed to create request JSON body: %w", err) - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, d.BaseURL.String(), buf) - if err != nil { - return fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") - - resp, err := d.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - if resp.StatusCode/100 != 2 { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - var rpcResp APIResponse - - err = json.Unmarshal(raw, &rpcResp) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - if rpcResp.Error != nil { - return rpcResp.Error - } - - err = json.Unmarshal(rpcResp.Result, result) - if err != nil { - return fmt.Errorf("unable to unmarshal response: %w: %s", err, rpcResp.Result) - } - - return nil -} - -func (d *Client) computeDigest(parts ...string) string { - params := []string{d.apiKey} - params = append(params, parts...) - params = append(params, d.apiSecret) - - mac := hmac.New(sha256.New, []byte(d.apiSecret)) - mac.Write([]byte(strings.Join(params, "&"))) - - return hex.EncodeToString(mac.Sum(nil)) -} diff --git a/providers/dns/namesurfer/internal/client_test.go b/providers/dns/namesurfer/internal/client_test.go deleted file mode 100644 index 9e8f917bc..000000000 --- a/providers/dns/namesurfer/internal/client_test.go +++ /dev/null @@ -1,158 +0,0 @@ -package internal - -import ( - "net/http/httptest" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient(server.URL, "user", "secret") - if err != nil { - return nil, err - } - - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(), - ) -} - -func TestClient_AddDNSRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /jsonrpc10", - servermock.ResponseFromFixture("addDNSRecord.json"), - servermock.CheckRequestJSONBodyFromFixture("addDNSRecord-request.json"), - ). - Build(t) - - record := DNSNode{ - Name: "_acme-challenge", - Type: "TXT", - Data: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: 300, - } - - err := client.AddDNSRecord(t.Context(), "example.com", "viewA", record) - require.NoError(t, err) -} - -func TestClient_AddDNSRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /jsonrpc10", - servermock.ResponseFromFixture("error.json"), - ). - Build(t) - - record := DNSNode{ - Name: "_acme-challenge", - Type: "TXT", - Data: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: 300, - } - - err := client.AddDNSRecord(t.Context(), "example.com", "viewA", record) - require.EqualError(t, err, "code: Server.Keyfailure, "+ - "filename: service, line: 13, "+ - "message: Unknown keyname user, "+ - `detail: Traceback (most recent call last): File "/usr/local/namesurfer/python/lib/python2.6/site-packages/ladon/server/dispatcher.py", line 159, in dispatch_request result = self.call_method(method,req_dict,tc,export_dict,log_line) File "/usr/local/namesurfer/python/lib/python2.6/site-packages/ladon/server/dispatcher.py", line 96, in call_method result = getattr(service_class_instance,req_dict['methodname'])(*args) File "/usr/local/namesurfer/python/lib/python2.6/site-packages/ladon/ladonizer/decorator.py", line 77, in injector res = f(*args,**kw) File "/usr/local/namesurfer/webui2/webui/service/service10/NSService_10.py", line 502, in addDNSRecord key = validate_key(keyname, digest, [zonename, viewname, record.name, record.type, str(record.ttl), record.data]) File "/usr/local/namesurfer/webui2/webui/service/base/implementation.py", line 63, in validate_key raise ApiFault('Server.Keyfailure', 'Unknown keyname %s' % keyname) ApiFault: service(13): Unknown keyname user `) -} - -func TestClient_UpdateDNSHost(t *testing.T) { - client := mockBuilder(). - Route("POST /jsonrpc10", - servermock.ResponseFromFixture("updateDNSHost.json"), - servermock.CheckRequestJSONBodyFromFixture("updateDNSHost-request.json"), - ). - Build(t) - - record := DNSNode{ - Name: "_acme-challenge", - Type: "TXT", - Data: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: 300, - } - - err := client.UpdateDNSHost(t.Context(), "example.com", "viewA", record, DNSNode{}) - require.NoError(t, err) -} - -func TestClient_SearchDNSHosts(t *testing.T) { - client := mockBuilder(). - Route("POST /jsonrpc10", - servermock.ResponseFromFixture("searchDNSHosts.json"), - servermock.CheckRequestJSONBodyFromFixture("searchDNSHosts-request.json"), - ). - Build(t) - - records, err := client.SearchDNSHosts(t.Context(), "value") - require.NoError(t, err) - - expected := []DNSNode{ - {Name: "foo", Type: "TXT", Data: "xxx", TTL: 300}, - {Name: "_acme-challenge", Type: "TXT", Data: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", TTL: 300}, - {Name: "bar", Type: "A", Data: "yyy", TTL: 300}, - } - - assert.Equal(t, expected, records) -} - -func TestClient_ListZones(t *testing.T) { - client := mockBuilder(). - Route("POST /jsonrpc10", - servermock.ResponseFromFixture("listZones.json"), - servermock.CheckRequestJSONBodyFromFixture("listZones-request.json"), - ). - Build(t) - - zones, err := client.ListZones(t.Context(), "value") - require.NoError(t, err) - - expected := []DNSZone{ - {Name: "example.com", View: "viewA"}, - {Name: "example.org", View: "viewB"}, - {Name: "example.net", View: "viewC"}, - } - - assert.Equal(t, expected, zones) -} - -func TestClient_computeDigest(t *testing.T) { - client, err := NewClient("https://test.example.com", "testkey", "testsecret") - require.NoError(t, err) - - testCases := []struct { - desc string - parts []string - expected string - }{ - { - desc: "no parts", - parts: []string{}, - expected: "99b5dcdc19bfc0ce2af3fe848f4bcb6f7beb352e9599e8ba50544d86de567282", - }, - { - desc: "parts", - parts: []string{"zone.example.com", "default"}, - expected: "94efef76383889b1ae620582a25d1c3aa9bd9ba9ac4bdccdf4aefbc3ae6e8329", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - digest := client.computeDigest(test.parts...) - - assert.Equal(t, test.expected, digest) - }) - } -} diff --git a/providers/dns/namesurfer/internal/fixtures/addDNSRecord-request.json b/providers/dns/namesurfer/internal/fixtures/addDNSRecord-request.json deleted file mode 100644 index 660109aae..000000000 --- a/providers/dns/namesurfer/internal/fixtures/addDNSRecord-request.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": 1, - "method": "addDNSRecord", - "params": [ - "user", - "4fcc5fa29531709b0381c8debea127a6a26e71cb9491727876819cf5805c4990", - "example.com", - "viewA", - { - "name": "_acme-challenge", - "type": "TXT", - "data": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "ttl": 300 - } - ] -} diff --git a/providers/dns/namesurfer/internal/fixtures/addDNSRecord.json b/providers/dns/namesurfer/internal/fixtures/addDNSRecord.json deleted file mode 100644 index f41779e30..000000000 --- a/providers/dns/namesurfer/internal/fixtures/addDNSRecord.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "id": 1, - "result": true -} diff --git a/providers/dns/namesurfer/internal/fixtures/error.json b/providers/dns/namesurfer/internal/fixtures/error.json deleted file mode 100644 index 8ddf8df25..000000000 --- a/providers/dns/namesurfer/internal/fixtures/error.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "result": null, - "error": { - "filename": "service", - "lineno": 13, - "code": "Server.Keyfailure", - "string": "Unknown keyname user", - "detail": [ - "Traceback (most recent call last):", - " File \"/usr/local/namesurfer/python/lib/python2.6/site-packages/ladon/server/dispatcher.py\", line 159, in dispatch_request", - " result = self.call_method(method,req_dict,tc,export_dict,log_line)", - " File \"/usr/local/namesurfer/python/lib/python2.6/site-packages/ladon/server/dispatcher.py\", line 96, in call_method", - " result = getattr(service_class_instance,req_dict['methodname'])(*args)", - " File \"/usr/local/namesurfer/python/lib/python2.6/site-packages/ladon/ladonizer/decorator.py\", line 77, in injector", - " res = f(*args,**kw)", - " File \"/usr/local/namesurfer/webui2/webui/service/service10/NSService_10.py\", line 502, in addDNSRecord", - " key = validate_key(keyname, digest, [zonename, viewname, record.name, record.type, str(record.ttl), record.data])", - " File \"/usr/local/namesurfer/webui2/webui/service/base/implementation.py\", line 63, in validate_key", - " raise ApiFault('Server.Keyfailure', 'Unknown keyname %s' % keyname)", - "ApiFault: service(13): Unknown keyname user", - "" - ] - } -} diff --git a/providers/dns/namesurfer/internal/fixtures/listZones-request.json b/providers/dns/namesurfer/internal/fixtures/listZones-request.json deleted file mode 100644 index 06689de7a..000000000 --- a/providers/dns/namesurfer/internal/fixtures/listZones-request.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "id": 1, - "method": "listZones", - "params": [ - "user", - "2739461ea1a3dc51302993f724f40228409c53b78025d8d7b1d7bba3c1bf2d66", - "value" - ] -} diff --git a/providers/dns/namesurfer/internal/fixtures/listZones.json b/providers/dns/namesurfer/internal/fixtures/listZones.json deleted file mode 100644 index 37fa2053b..000000000 --- a/providers/dns/namesurfer/internal/fixtures/listZones.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": 1, - "result": [ - { - "name": "example.com", - "view": "viewA" - }, - { - "name": "example.org", - "view": "viewB" - }, - { - "name": "example.net", - "view": "viewC" - } - ] -} diff --git a/providers/dns/namesurfer/internal/fixtures/searchDNSHosts-request.json b/providers/dns/namesurfer/internal/fixtures/searchDNSHosts-request.json deleted file mode 100644 index 4a88340e2..000000000 --- a/providers/dns/namesurfer/internal/fixtures/searchDNSHosts-request.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "id": 1, - "method": "searchDNSHosts", - "params": [ - "user", - "02cf1a2f6e124507d16738d595f583932185313fc96afc2d8404960acaec29b4", - "value" - ] -} diff --git a/providers/dns/namesurfer/internal/fixtures/searchDNSHosts.json b/providers/dns/namesurfer/internal/fixtures/searchDNSHosts.json deleted file mode 100644 index 822459148..000000000 --- a/providers/dns/namesurfer/internal/fixtures/searchDNSHosts.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "id": 1, - "result": [ - { - "name": "foo", - "type": "TXT", - "data": "xxx", - "ttl": 300 - }, - { - "name": "_acme-challenge", - "type": "TXT", - "data": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "ttl": 300 - }, - { - "name": "bar", - "type": "A", - "data": "yyy", - "ttl": 300 - } - ] -} diff --git a/providers/dns/namesurfer/internal/fixtures/updateDNSHost-request.json b/providers/dns/namesurfer/internal/fixtures/updateDNSHost-request.json deleted file mode 100644 index 494de20c6..000000000 --- a/providers/dns/namesurfer/internal/fixtures/updateDNSHost-request.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "id": 1, - "method": "updateDNSHost", - "params": [ - "user", - "510e63288ac874c1d5ba313a9411591daa346e5621fb0153263adc278794e378", - "example.com", - "viewA", - { - "name": "_acme-challenge", - "type": "TXT", - "data": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "ttl": 300 - }, - { - "name": "", - "type": "", - "data": "", - "ttl": 0 - } - ] -} diff --git a/providers/dns/namesurfer/internal/fixtures/updateDNSHost.json b/providers/dns/namesurfer/internal/fixtures/updateDNSHost.json deleted file mode 100644 index f41779e30..000000000 --- a/providers/dns/namesurfer/internal/fixtures/updateDNSHost.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "id": 1, - "result": true -} diff --git a/providers/dns/namesurfer/internal/types.go b/providers/dns/namesurfer/internal/types.go deleted file mode 100644 index d364c1876..000000000 --- a/providers/dns/namesurfer/internal/types.go +++ /dev/null @@ -1,72 +0,0 @@ -package internal - -import ( - "encoding/json" - "fmt" - "strings" -) - -// DNSNode represents a DNS record. -// http://95.128.3.201:8053/API/NSService_10#DNSNode -type DNSNode struct { - Name string `json:"name"` - Type string `json:"type"` - Data string `json:"data"` - TTL int `json:"ttl"` -} - -// DNSZone represents a DNS zone. -// http://95.128.3.201:8053/API/NSService_10#DNSZone -type DNSZone struct { - Name string `json:"name,omitempty"` - View string `json:"view,omitempty"` -} - -// APIRequest represents a JSON-RPC request. -// https://www.jsonrpc.org/specification_v1#a1.1Requestmethodinvocation -type APIRequest struct { - ID any `json:"id"` // Can be int or string depending on API - Method string `json:"method"` - Params []any `json:"params"` -} - -// APIResponse represents a JSON-RPC response. -// https://www.jsonrpc.org/specification_v1#a1.2Response -type APIResponse struct { - ID any `json:"id"` // Can be int or string depending on API - Result json.RawMessage `json:"result"` - Error *APIError `json:"error"` -} - -// APIError represents an error. -type APIError struct { - Code any `json:"code"` // Can be int or string depending on API - Filename string `json:"filename"` - LineNumber int `json:"lineno"` - Message string `json:"string"` - Detail []string `json:"detail"` -} - -func (e *APIError) Error() string { - msg := new(strings.Builder) - - _, _ = fmt.Fprintf(msg, "code: %v", e.Code) - - if e.Filename != "" { - _, _ = fmt.Fprintf(msg, ", filename: %s", e.Filename) - } - - if e.LineNumber > 0 { - _, _ = fmt.Fprintf(msg, ", line: %d", e.LineNumber) - } - - if e.Message != "" { - _, _ = fmt.Fprintf(msg, ", message: %s", e.Message) - } - - if len(e.Detail) > 0 { - _, _ = fmt.Fprintf(msg, ", detail: %v", strings.Join(e.Detail, " ")) - } - - return msg.String() -} diff --git a/providers/dns/namesurfer/namesurfer.go b/providers/dns/namesurfer/namesurfer.go deleted file mode 100644 index 6b7f48402..000000000 --- a/providers/dns/namesurfer/namesurfer.go +++ /dev/null @@ -1,214 +0,0 @@ -// Package namesurfer implements a DNS provider for solving the DNS-01 challenge using FusionLayer NameSurfer API. -package namesurfer - -import ( - "context" - "crypto/tls" - "errors" - "fmt" - "net/http" - "strings" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/namesurfer/internal" -) - -// Environment variables names. -const ( - envNamespace = "NAMESURFER_" - - EnvBaseURL = envNamespace + "BASE_URL" - EnvAPIKey = envNamespace + "API_KEY" - EnvAPISecret = envNamespace + "API_SECRET" - EnvView = envNamespace + "VIEW" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" - EnvInsecureSkipVerify = envNamespace + "INSECURE_SKIP_VERIFY" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - BaseURL string - APIKey string - APISecret string - View string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, 300), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client - - zones map[string]string - zonesMu sync.Mutex -} - -// NewDNSProvider returns a DNSProvider instance configured for FusionLayer NameSurfer. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvBaseURL, EnvAPIKey, EnvAPISecret) - if err != nil { - return nil, fmt.Errorf("namesurfer: %w", err) - } - - config := NewDefaultConfig() - config.BaseURL = values[EnvBaseURL] - config.APIKey = values[EnvAPIKey] - config.APISecret = values[EnvAPISecret] - config.View = env.GetOrDefaultString(EnvView, "") - - if env.GetOrDefaultBool(EnvInsecureSkipVerify, false) { - config.HTTPClient.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - } - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for FusionLayer NameSurfer. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("namesurfer: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.BaseURL, config.APIKey, config.APISecret) - if err != nil { - return nil, fmt.Errorf("namesurfer: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - zones: make(map[string]string), - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - zone, err := d.findZone(ctx, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("namesurfer: %w", err) - } - - d.zonesMu.Lock() - d.zones[token] = zone - d.zonesMu.Unlock() - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone) - if err != nil { - return fmt.Errorf("namesurfer: %w", err) - } - - record := internal.DNSNode{ - Name: subDomain, - Type: "TXT", - TTL: d.config.TTL, - Data: info.Value, - } - - err = d.client.AddDNSRecord(ctx, zone, d.config.View, record) - if err != nil { - return fmt.Errorf("namesurfer: add DNS record: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - d.zonesMu.Lock() - zone, ok := d.zones[token] - d.zonesMu.Unlock() - - if !ok { - return fmt.Errorf("namesurfer: unknown zone for '%s'", info.EffectiveFQDN) - } - - d.zonesMu.Lock() - delete(d.zones, token) - d.zonesMu.Unlock() - - existing, err := d.client.SearchDNSHosts(ctx, dns01.UnFqdn(info.EffectiveFQDN)) - if err != nil { - return fmt.Errorf("namesurfer: search DNS hosts: %w", err) - } - - for _, node := range existing { - if node.Type != "TXT" || node.Data != info.Value { - continue - } - - err = d.client.UpdateDNSHost(ctx, zone, d.config.View, node, internal.DNSNode{}) - if err != nil { - return fmt.Errorf("namesurfer: update DNS host: %w", err) - } - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -func (d *DNSProvider) findZone(ctx context.Context, fqdn string) (string, error) { - zones, err := d.client.ListZones(ctx, "forward") - if err != nil { - return "", fmt.Errorf("list zones: %w", err) - } - - domain := dns01.UnFqdn(fqdn) - - var zoneName string - - for _, zone := range zones { - if strings.HasSuffix(domain, zone.Name) && len(zone.Name) > len(zoneName) { - zoneName = zone.Name - } - } - - if zoneName == "" { - return "", fmt.Errorf("no zone found for %s", fqdn) - } - - return zoneName, nil -} diff --git a/providers/dns/namesurfer/namesurfer.toml b/providers/dns/namesurfer/namesurfer.toml deleted file mode 100644 index fd914ec0c..000000000 --- a/providers/dns/namesurfer/namesurfer.toml +++ /dev/null @@ -1,28 +0,0 @@ -Name = "FusionLayer NameSurfer" -Description = '''''' -URL = "https://www.fusionlayer.com/" -Code = "namesurfer" -Since = "v4.32.0" - -Example = ''' -NAMESURFER_BASE_URL=https://foo.example.com:8443/API/NSService_10 \ -NAMESURFER_API_KEY=xxx \ -NAMESURFER_API_SECRET=yyy \ -lego --dns namesurfer -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - NAMESURFER_BASE_URL = "The base URL of NameSurfer API (jsonrpc10) endpoint URL (e.g., https://foo.example.com:8443/API/NSService_10)" - NAMESURFER_API_KEY = "API key name" - NAMESURFER_API_SECRET = "API secret" - [Configuration.Additional] - NAMESURFER_VIEW = "DNS view name (optional, default: empty string)" - NAMESURFER_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - NAMESURFER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - NAMESURFER_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - NAMESURFER_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - NAMESURFER_INSECURE_SKIP_VERIFY = "Whether to verify the API certificate" - -[Links] - API = "https://web.archive.org/web/20260213170737/http://95.128.3.201:8053/API/NSService_10" diff --git a/providers/dns/namesurfer/namesurfer_test.go b/providers/dns/namesurfer/namesurfer_test.go deleted file mode 100644 index ce3aa37af..000000000 --- a/providers/dns/namesurfer/namesurfer_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package namesurfer - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest( - EnvBaseURL, - EnvAPIKey, - EnvAPISecret, - EnvView, -).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvBaseURL: "https://example.com", - EnvAPIKey: "user", - EnvAPISecret: "secret", - }, - }, - { - desc: "missing base URL", - envVars: map[string]string{ - EnvBaseURL: "", - EnvAPIKey: "user", - EnvAPISecret: "secret", - }, - expected: "namesurfer: some credentials information are missing: NAMESURFER_BASE_URL", - }, - { - desc: "missing API key", - envVars: map[string]string{ - EnvBaseURL: "https://example.com", - EnvAPIKey: "", - EnvAPISecret: "secret", - }, - expected: "namesurfer: some credentials information are missing: NAMESURFER_API_KEY", - }, - { - desc: "missing API secret", - envVars: map[string]string{ - EnvBaseURL: "https://example.com", - EnvAPIKey: "user", - EnvAPISecret: "", - }, - expected: "namesurfer: some credentials information are missing: NAMESURFER_API_SECRET", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "namesurfer: some credentials information are missing: NAMESURFER_BASE_URL,NAMESURFER_API_KEY,NAMESURFER_API_SECRET", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - baseURL string - apiKey string - apiSecret string - expected string - }{ - { - desc: "success", - baseURL: "https://example.com", - apiKey: "user", - apiSecret: "secret", - }, - { - desc: "missing base URL", - apiKey: "user", - apiSecret: "secret", - expected: "namesurfer: base URL missing", - }, - { - desc: "missing API key", - baseURL: "https://example.com", - apiSecret: "secret", - expected: "namesurfer: credentials missing", - }, - { - desc: "missing API secret", - baseURL: "https://example.com", - apiKey: "user", - expected: "namesurfer: credentials missing", - }, - { - desc: "missing credentials", - expected: "namesurfer: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.BaseURL = test.baseURL - config.APIKey = test.apiKey - config.APISecret = test.apiSecret - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/nearlyfreespeech/internal/client.go b/providers/dns/nearlyfreespeech/internal/client.go index 5d7e79fbe..08d8d511f 100644 --- a/providers/dns/nearlyfreespeech/internal/client.go +++ b/providers/dns/nearlyfreespeech/internal/client.go @@ -34,7 +34,7 @@ type Client struct { HTTPClient *http.Client } -func NewClient(login, apiKey string) *Client { +func NewClient(login string, apiKey string) *Client { baseURL, _ := url.Parse(apiURL) return &Client{ @@ -46,7 +46,7 @@ func NewClient(login, apiKey string) *Client { } } -func (c *Client) AddRecord(ctx context.Context, domain string, record Record) error { +func (c Client) AddRecord(ctx context.Context, domain string, record Record) error { endpoint := c.baseURL.JoinPath("dns", dns01.UnFqdn(domain), "addRR") params, err := querystring.Values(record) @@ -57,7 +57,7 @@ func (c *Client) AddRecord(ctx context.Context, domain string, record Record) er return c.doRequest(ctx, endpoint, params) } -func (c *Client) RemoveRecord(ctx context.Context, domain string, record Record) error { +func (c Client) RemoveRecord(ctx context.Context, domain string, record Record) error { endpoint := c.baseURL.JoinPath("dns", dns01.UnFqdn(domain), "removeRR") params, err := querystring.Values(record) @@ -68,7 +68,7 @@ func (c *Client) RemoveRecord(ctx context.Context, domain string, record Record) return c.doRequest(ctx, endpoint, params) } -func (c *Client) doRequest(ctx context.Context, endpoint *url.URL, params url.Values) error { +func (c Client) doRequest(ctx context.Context, endpoint *url.URL, params url.Values) error { payload := params.Encode() req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), strings.NewReader(payload)) @@ -97,7 +97,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) errAPI := &APIError{} - err := json.Unmarshal(raw, errAPI) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) @@ -115,10 +114,11 @@ func NewSigner() *Signer { return &Signer{saltShaker: getRandomSalt, clock: time.Now} } -func (c Signer) Sign(uri, body, login, apiKey string) string { +func (c Signer) Sign(uri string, body, login, apiKey string) string { // Header is "login;timestamp;salt;hash". // hash is SHA1("login;timestamp;salt;api-key;request-uri;body-hash") // and body-hash is SHA1(body). + bodyHash := sha1.Sum([]byte(body)) timestamp := strconv.FormatInt(c.clock().Unix(), 10) diff --git a/providers/dns/nearlyfreespeech/internal/client_test.go b/providers/dns/nearlyfreespeech/internal/client_test.go index 26e4552be..935ee4fff 100644 --- a/providers/dns/nearlyfreespeech/internal/client_test.go +++ b/providers/dns/nearlyfreespeech/internal/client_test.go @@ -1,18 +1,27 @@ package internal import ( + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" "testing" "time" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func setupClient(server *httptest.Server) (*Client, error) { +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + client := NewClient("user", "secret") client.HTTPClient = server.Client() client.baseURL, _ = url.Parse(server.URL) @@ -20,22 +29,66 @@ func setupClient(server *httptest.Server) (*Client, error) { client.signer.saltShaker = func() []byte { return []byte("0123456789ABCDEF") } client.signer.clock = func() time.Time { return time.Unix(1692475113, 0) } - return client, nil + return client, mux +} + +func testHandler(params map[string]string) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + if req.Header.Get(authenticationHeader) == "" { + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + err := req.ParseForm() + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + for k, v := range params { + if req.PostForm.Get(k) != v { + http.Error(rw, fmt.Sprintf("data: got %s want %s", k, v), http.StatusBadRequest) + return + } + } + } +} + +func testErrorHandler() http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + file, err := os.Open("./fixtures/error.json") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + rw.WriteHeader(http.StatusUnauthorized) + + _, _ = io.Copy(rw, file) + } } func TestClient_AddRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient, - servermock.CheckHeader(). - WithContentTypeFromURLEncoded(). - With(authenticationHeader, "user;1692475113;0123456789ABCDEF;24a32faf74c7bd0525f560ff12a1c1fb6545bafc"), - ). - Route("POST /dns/example.com/addRR", nil, servermock.CheckForm().Strict(). - With("data", "txtTXTtxt"). - With("name", "sub"). - With("type", "TXT"). - With("ttl", "30"), - ). - Build(t) + client, mux := setupTest(t) + + params := map[string]string{ + "data": "txtTXTtxt", + "name": "sub", + "type": "TXT", + "ttl": "30", + } + + mux.Handle("/dns/example.com/addRR", testHandler(params)) record := Record{ Name: "sub", @@ -44,20 +97,14 @@ func TestClient_AddRecord(t *testing.T) { TTL: 30, } - err := client.AddRecord(t.Context(), "example.com", record) + err := client.AddRecord(context.Background(), "example.com", record) require.NoError(t, err) } func TestClient_AddRecord_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient, - servermock.CheckHeader(). - WithContentTypeFromURLEncoded(). - With(authenticationHeader, "user;1692475113;0123456789ABCDEF;24a32faf74c7bd0525f560ff12a1c1fb6545bafc"), - ). - Route("POST /dns/example.com/addRR", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client, mux := setupTest(t) + + mux.Handle("/dns/example.com/addRR", testErrorHandler()) record := Record{ Name: "sub", @@ -66,23 +113,20 @@ func TestClient_AddRecord_error(t *testing.T) { TTL: 30, } - err := client.AddRecord(t.Context(), "example.com", record) + err := client.AddRecord(context.Background(), "example.com", record) require.Error(t, err) } func TestClient_RemoveRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient, - servermock.CheckHeader(). - WithContentTypeFromURLEncoded(). - With(authenticationHeader, "user;1692475113;0123456789ABCDEF;699f01f077ca487bd66ac370d6dfc5b122c65522"), - ). - Route("POST /dns/example.com/removeRR", nil, - servermock.CheckForm().Strict(). - With("data", "txtTXTtxt"). - With("name", "sub"). - With("type", "TXT"), - ). - Build(t) + client, mux := setupTest(t) + + params := map[string]string{ + "data": "txtTXTtxt", + "name": "sub", + "type": "TXT", + } + + mux.Handle("/dns/example.com/removeRR", testHandler(params)) record := Record{ Name: "sub", @@ -90,20 +134,14 @@ func TestClient_RemoveRecord(t *testing.T) { Data: "txtTXTtxt", } - err := client.RemoveRecord(t.Context(), "example.com", record) + err := client.RemoveRecord(context.Background(), "example.com", record) require.NoError(t, err) } func TestClient_RemoveRecord_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient, - servermock.CheckHeader(). - WithContentTypeFromURLEncoded(). - With(authenticationHeader, "user;1692475113;0123456789ABCDEF;699f01f077ca487bd66ac370d6dfc5b122c65522"), - ). - Route("POST /dns/example.com/removeRR", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client, mux := setupTest(t) + + mux.Handle("/dns/example.com/removeRR", testErrorHandler()) record := Record{ Name: "sub", @@ -111,7 +149,7 @@ func TestClient_RemoveRecord_error(t *testing.T) { Data: "txtTXTtxt", } - err := client.RemoveRecord(t.Context(), "example.com", record) + err := client.RemoveRecord(context.Background(), "example.com", record) require.Error(t, err) } @@ -163,7 +201,6 @@ func TestSigner_Sign(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { t.Parallel() - signer := NewSigner() signer.saltShaker = func() []byte { return []byte(test.salt) } signer.clock = func() time.Time { return time.Unix(test.now, 0) } diff --git a/providers/dns/nearlyfreespeech/nearlyfreespeech.go b/providers/dns/nearlyfreespeech/nearlyfreespeech.go index af5e5363c..8f94e0911 100644 --- a/providers/dns/nearlyfreespeech/nearlyfreespeech.go +++ b/providers/dns/nearlyfreespeech/nearlyfreespeech.go @@ -8,10 +8,8 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/nearlyfreespeech/internal" ) @@ -29,8 +27,6 @@ const ( EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string @@ -93,8 +89,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, client: client, diff --git a/providers/dns/nearlyfreespeech/nearlyfreespeech.toml b/providers/dns/nearlyfreespeech/nearlyfreespeech.toml index 3a1e25942..e81579f66 100644 --- a/providers/dns/nearlyfreespeech/nearlyfreespeech.toml +++ b/providers/dns/nearlyfreespeech/nearlyfreespeech.toml @@ -7,7 +7,7 @@ Since = "v4.8.0" Example = ''' NEARLYFREESPEECH_API_KEY=xxxxxx \ NEARLYFREESPEECH_LOGIN=xxxx \ -lego --dns nearlyfreespeech -d '*.example.com' -d example.com run +lego --email you@example.com --dns nearlyfreespeech --domains my.example.org run ''' [Configuration] @@ -15,11 +15,11 @@ lego --dns nearlyfreespeech -d '*.example.com' -d example.com run NEARLYFREESPEECH_API_KEY = "API Key for API requests" NEARLYFREESPEECH_LOGIN = "Username for API requests" [Configuration.Additional] - NEARLYFREESPEECH_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - NEARLYFREESPEECH_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - NEARLYFREESPEECH_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)" - NEARLYFREESPEECH_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 60)" - NEARLYFREESPEECH_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + NEARLYFREESPEECH_POLLING_INTERVAL = "Time between DNS propagation check" + NEARLYFREESPEECH_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + NEARLYFREESPEECH_TTL = "The TTL of the TXT record used for the DNS challenge" + NEARLYFREESPEECH_HTTP_TIMEOUT = "API request timeout" + NEARLYFREESPEECH_SEQUENCE_INTERVAL = "Time between sequential requests" [Links] API = "https://members.nearlyfreespeech.net/wiki/API/Reference" diff --git a/providers/dns/nearlyfreespeech/nearlyfreespeech_test.go b/providers/dns/nearlyfreespeech/nearlyfreespeech_test.go index b67b350e9..adc7efe1e 100644 --- a/providers/dns/nearlyfreespeech/nearlyfreespeech_test.go +++ b/providers/dns/nearlyfreespeech/nearlyfreespeech_test.go @@ -54,7 +54,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -127,7 +126,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -141,7 +139,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/neodigit/neodigit.go b/providers/dns/neodigit/neodigit.go deleted file mode 100644 index d41846307..000000000 --- a/providers/dns/neodigit/neodigit.go +++ /dev/null @@ -1,103 +0,0 @@ -// Package neodigit implements a DNS provider for solving the DNS-01 challenge using Neodigit DNS. -package neodigit - -import ( - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/tecnocratica" -) - -// Environment variables names. -const ( - envNamespace = "NEODIGIT_" - - EnvToken = envNamespace + "TOKEN" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -const defaultBaseURL = "https://api.neodigit.net/v1" - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config = tecnocratica.Config - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - prv challenge.ProviderTimeout -} - -// NewDNSProvider returns a DNSProvider instance configured for Neodigit. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvToken) - if err != nil { - return nil, fmt.Errorf("neodigit: %w", err) - } - - config := NewDefaultConfig() - config.Token = values[EnvToken] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Neodigit. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("neodigit: the configuration of the DNS provider is nil") - } - - provider, err := tecnocratica.NewDNSProviderConfig(config, defaultBaseURL) - if err != nil { - return nil, fmt.Errorf("neodigit: %w", err) - } - - return &DNSProvider{prv: provider}, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - err := d.prv.Present(domain, token, keyAuth) - if err != nil { - return fmt.Errorf("neodigit: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - err := d.prv.CleanUp(domain, token, keyAuth) - if err != nil { - return fmt.Errorf("neodigit: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.prv.Timeout() -} diff --git a/providers/dns/neodigit/neodigit.toml b/providers/dns/neodigit/neodigit.toml deleted file mode 100644 index 91b3cfb07..000000000 --- a/providers/dns/neodigit/neodigit.toml +++ /dev/null @@ -1,22 +0,0 @@ -Name = "Neodigit" -Description = '''''' -URL = "https://www.neodigit.net" -Code = "neodigit" -Since = "v4.30.0" - -Example = ''' -NEODIGIT_TOKEN=xxxxxx \ -lego --dns neodigit -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - NEODIGIT_TOKEN = "API token" - [Configuration.Additional] - NEODIGIT_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" - NEODIGIT_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 300)" - NEODIGIT_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - NEODIGIT_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://developers.neodigit.net/#dns" diff --git a/providers/dns/neodigit/neodigit_test.go b/providers/dns/neodigit/neodigit_test.go deleted file mode 100644 index 39f67c59c..000000000 --- a/providers/dns/neodigit/neodigit_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package neodigit - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvToken: "secret", - }, - }, - { - desc: "missing credentials: token", - envVars: map[string]string{ - EnvToken: "", - }, - expected: "neodigit: some credentials information are missing: NEODIGIT_TOKEN", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.prv) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - token string - expected string - }{ - { - desc: "success", - token: "secret", - }, - { - desc: "missing token", - expected: "neodigit: missing credentials", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.Token = test.token - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.prv) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/netcup/internal/client.go b/providers/dns/netcup/internal/client.go index 1287a8d7a..9573c09c8 100644 --- a/providers/dns/netcup/internal/client.go +++ b/providers/dns/netcup/internal/client.go @@ -80,7 +80,6 @@ func (c *Client) GetDNSRecords(ctx context.Context, hostname string) ([]DNSRecor } var responseData InfoDNSRecordsResponse - err := c.doRequest(ctx, payload, &responseData) if err != nil { return nil, fmt.Errorf("error when sending the request: %w", err) @@ -140,11 +139,10 @@ func GetDNSRecordIdx(records []DNSRecord, record DNSRecord) (int, error) { return index, nil } } - return -1, errors.New("no DNS Record found") } -func newJSONRequest(ctx context.Context, method, endpoint string, payload any) (*http.Request, error) { +func newJSONRequest(ctx context.Context, method string, endpoint string, payload any) (*http.Request, error) { buf := new(bytes.Buffer) if payload != nil { @@ -175,7 +173,6 @@ func unmarshalResponseMsg(req *http.Request, resp *http.Response) (*ResponseMsg, } var respMsg ResponseMsg - err = json.Unmarshal(raw, &respMsg) if err != nil { return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) diff --git a/providers/dns/netcup/internal/client_live_test.go b/providers/dns/netcup/internal/client_live_test.go deleted file mode 100644 index 68621882e..000000000 --- a/providers/dns/netcup/internal/client_live_test.go +++ /dev/null @@ -1,137 +0,0 @@ -package internal - -import ( - "fmt" - "strconv" - "strings" - "testing" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var envTest = tester.NewEnvTest( - "NETCUP_CUSTOMER_NUMBER", - "NETCUP_API_KEY", - "NETCUP_API_PASSWORD"). - WithDomain("NETCUP_DOMAIN") - -func TestClient_GetDNSRecords_Live(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - // Setup - envTest.RestoreEnv() - - client, err := NewClient( - envTest.GetValue("NETCUP_CUSTOMER_NUMBER"), - envTest.GetValue("NETCUP_API_KEY"), - envTest.GetValue("NETCUP_API_PASSWORD")) - require.NoError(t, err) - - ctx, err := client.CreateSessionContext(t.Context()) - require.NoError(t, err) - - info := dns01.GetChallengeInfo(envTest.GetDomain(), "123d==") - - zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - require.NoError(t, err) - - zone = dns01.UnFqdn(zone) - - // TestMethod - _, err = client.GetDNSRecords(ctx, zone) - require.NoError(t, err) - - // Tear down - err = client.Logout(ctx) - require.NoError(t, err) -} - -func TestClient_UpdateDNSRecord_Live(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - // Setup - envTest.RestoreEnv() - - client, err := NewClient( - envTest.GetValue("NETCUP_CUSTOMER_NUMBER"), - envTest.GetValue("NETCUP_API_KEY"), - envTest.GetValue("NETCUP_API_PASSWORD")) - require.NoError(t, err) - - ctx, err := client.CreateSessionContext(t.Context()) - require.NoError(t, err) - - info := dns01.GetChallengeInfo(envTest.GetDomain(), "123d==") - - zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - require.NotErrorIs(t, err, fmt.Errorf("error finding DNSZone, %w", err)) - - hostname := strings.Replace(info.EffectiveFQDN, "."+zone, "", 1) - - record := DNSRecord{ - Hostname: hostname, - RecordType: "TXT", - Destination: "asdf5678", - DeleteRecord: false, - } - - // test - zone = dns01.UnFqdn(zone) - - err = client.UpdateDNSRecord(ctx, zone, []DNSRecord{record}) - require.NoError(t, err) - - records, err := client.GetDNSRecords(ctx, zone) - require.NoError(t, err) - - recordIdx, err := GetDNSRecordIdx(records, record) - require.NoError(t, err) - - assert.Equal(t, record.Hostname, records[recordIdx].Hostname) - assert.Equal(t, record.RecordType, records[recordIdx].RecordType) - assert.Equal(t, record.Destination, records[recordIdx].Destination) - assert.Equal(t, record.DeleteRecord, records[recordIdx].DeleteRecord) - - records[recordIdx].DeleteRecord = true - - // Tear down - err = client.UpdateDNSRecord(ctx, envTest.GetDomain(), []DNSRecord{records[recordIdx]}) - require.NoError(t, err) - - err = client.Logout(ctx) - require.NoError(t, err) -} - -func TestLiveClientAuth(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - // Setup - envTest.RestoreEnv() - - client, err := NewClient( - envTest.GetValue("NETCUP_CUSTOMER_NUMBER"), - envTest.GetValue("NETCUP_API_KEY"), - envTest.GetValue("NETCUP_API_PASSWORD")) - require.NoError(t, err) - - for i := range 4 { - t.Run("Test_"+strconv.Itoa(i+1), func(t *testing.T) { - t.Parallel() - - ctx, err := client.CreateSessionContext(t.Context()) - require.NoError(t, err) - - err = client.Logout(ctx) - require.NoError(t, err) - }) - } -} diff --git a/providers/dns/netcup/internal/client_test.go b/providers/dns/netcup/internal/client_test.go index 83c59460e..0e028e881 100644 --- a/providers/dns/netcup/internal/client_test.go +++ b/providers/dns/netcup/internal/client_test.go @@ -1,30 +1,41 @@ package internal import ( + "bytes" + "context" + "fmt" + "io" "net/http" "net/http/httptest" + "strings" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("a", "b", "c") - if err != nil { - return nil, err - } +var envTest = tester.NewEnvTest( + "NETCUP_CUSTOMER_NUMBER", + "NETCUP_API_KEY", + "NETCUP_API_PASSWORD"). + WithDomain("NETCUP_DOMAIN") - client.baseURL = server.URL - client.HTTPClient = server.Client() +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(), - ) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client, err := NewClient("a", "b", "c") + require.NoError(t, err) + + client.baseURL = server.URL + client.HTTPClient = server.Client() + + return client, mux } func TestGetDNSRecordIdx(t *testing.T) { @@ -129,10 +140,59 @@ func TestGetDNSRecordIdx(t *testing.T) { } func TestClient_GetDNSRecords(t *testing.T) { - client := mockBuilder(). - Route("POST /", servermock.ResponseFromFixture("get_dns_records.json"), - servermock.CheckRequestJSONBodyFromFixture("get_dns_records-request.json")). - Build(t) + client, mux := setupTest(t) + + mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { + raw, err := io.ReadAll(req.Body) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + if string(bytes.TrimSpace(raw)) != `{"action":"infoDnsRecords","param":{"domainname":"example.com","customernumber":"a","apikey":"b","apisessionid":""}}` { + http.Error(rw, fmt.Sprintf("invalid request body: %s", string(raw)), http.StatusBadRequest) + return + } + + response := ` + { + "serverrequestid":"srv-request-id", + "clientrequestid":"", + "action":"infoDnsRecords", + "status":"success", + "statuscode":2000, + "shortmessage":"Login successful", + "longmessage":"Session has been created successful.", + "responsedata":{ + "apisessionid":"api-session-id", + "dnsrecords":[ + { + "id":"1", + "hostname":"example.com", + "type":"TXT", + "priority":"1", + "destination":"bGVnbzE=", + "state":"yes", + "ttl":300 + }, + { + "id":"2", + "hostname":"example2.com", + "type":"TXT", + "priority":"1", + "destination":"bGVnbw==", + "state":"yes", + "ttl":300 + } + ] + } + }` + _, err = rw.Write([]byte(response)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) expected := []DNSRecord{{ ID: 1, @@ -142,6 +202,7 @@ func TestClient_GetDNSRecords(t *testing.T) { Destination: "bGVnbzE=", DeleteRecord: false, State: "yes", + TTL: 300, }, { ID: 2, Hostname: "example2.com", @@ -150,9 +211,10 @@ func TestClient_GetDNSRecords(t *testing.T) { Destination: "bGVnbw==", DeleteRecord: false, State: "yes", + TTL: 300, }} - records, err := client.GetDNSRecords(t.Context(), "example.com") + records, err := client.GetDNSRecords(context.Background(), "example.com") require.NoError(t, err) assert.Equal(t, expected, records) @@ -160,24 +222,67 @@ func TestClient_GetDNSRecords(t *testing.T) { func TestClient_GetDNSRecords_errors(t *testing.T) { testCases := []struct { - desc string - handler http.Handler - expected string + desc string + handler func(rw http.ResponseWriter, req *http.Request) }{ { - desc: "HTTP error", - handler: servermock.Noop().WithStatusCode(http.StatusInternalServerError), - expected: `error when sending the request: unexpected status code: [status code: 500] body: `, + desc: "HTTP error", + handler: func(rw http.ResponseWriter, _ *http.Request) { + http.Error(rw, "error message", http.StatusInternalServerError) + }, }, { - desc: "API error", - handler: servermock.ResponseFromFixture("get_dns_records_error.json"), - expected: `error when sending the request: an error occurred during the action infoDnsRecords: [Status=error, StatusCode=4013, ShortMessage=Validation Error., LongMessage=Message is empty.]`, + desc: "API error", + handler: func(rw http.ResponseWriter, _ *http.Request) { + response := ` + { + "serverrequestid":"YxTr4EzdbJ101T211zR4yzUEMVE", + "clientrequestid":"", + "action":"infoDnsRecords", + "status":"error", + "statuscode":4013, + "shortmessage":"Validation Error.", + "longmessage":"Message is empty.", + "responsedata":"" + }` + _, err := rw.Write([]byte(response)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }, }, { - desc: "responsedata marshaling error", - handler: servermock.ResponseFromFixture("get_dns_records_error_unmarshal.json"), - expected: `error when sending the request: unable to unmarshal response: [status code: 200] body: "" error: json: cannot unmarshal string into Go value of type internal.InfoDNSRecordsResponse`, + desc: "responsedata marshaling error", + handler: func(rw http.ResponseWriter, req *http.Request) { + raw, err := io.ReadAll(req.Body) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + if string(raw) != `{"action":"infoDnsRecords","param":{"domainname":"example.com","customernumber":"a","apikey":"b","apisessionid":"api-session-id"}}` { + http.Error(rw, fmt.Sprintf("invalid request body: %s", string(raw)), http.StatusBadRequest) + return + } + + response := ` + { + "serverrequestid":"srv-request-id", + "clientrequestid":"", + "action":"infoDnsRecords", + "status":"success", + "statuscode":2000, + "shortmessage":"Login successful", + "longmessage":"Session has been created successful.", + "responsedata":"" + }` + _, err = rw.Write([]byte(response)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }, }, } @@ -185,13 +290,105 @@ func TestClient_GetDNSRecords_errors(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - client := mockBuilder(). - Route("POST /", test.handler). - Build(t) + client, mux := setupTest(t) - records, err := client.GetDNSRecords(t.Context(), "example.com") - require.EqualError(t, err, test.expected) + mux.HandleFunc("/", test.handler) + + records, err := client.GetDNSRecords(context.Background(), "example.com") + require.Error(t, err) assert.Empty(t, records) }) } } + +func TestClient_GetDNSRecords_Live(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + // Setup + envTest.RestoreEnv() + + client, err := NewClient( + envTest.GetValue("NETCUP_CUSTOMER_NUMBER"), + envTest.GetValue("NETCUP_API_KEY"), + envTest.GetValue("NETCUP_API_PASSWORD")) + require.NoError(t, err) + + ctx, err := client.CreateSessionContext(context.Background()) + require.NoError(t, err) + + info := dns01.GetChallengeInfo(envTest.GetDomain(), "123d==") + + zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + require.NoError(t, err, "error finding DNSZone") + + zone = dns01.UnFqdn(zone) + + // TestMethod + _, err = client.GetDNSRecords(ctx, zone) + require.NoError(t, err) + + // Tear down + err = client.Logout(ctx) + require.NoError(t, err) +} + +func TestClient_UpdateDNSRecord_Live(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + // Setup + envTest.RestoreEnv() + + client, err := NewClient( + envTest.GetValue("NETCUP_CUSTOMER_NUMBER"), + envTest.GetValue("NETCUP_API_KEY"), + envTest.GetValue("NETCUP_API_PASSWORD")) + require.NoError(t, err) + + ctx, err := client.CreateSessionContext(context.Background()) + require.NoError(t, err) + + info := dns01.GetChallengeInfo(envTest.GetDomain(), "123d==") + + zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + require.NotErrorIs(t, err, fmt.Errorf("error finding DNSZone, %w", err)) + + hostname := strings.Replace(info.EffectiveFQDN, "."+zone, "", 1) + + record := DNSRecord{ + Hostname: hostname, + RecordType: "TXT", + Destination: "asdf5678", + DeleteRecord: false, + TTL: 120, + } + + // test + zone = dns01.UnFqdn(zone) + + err = client.UpdateDNSRecord(ctx, zone, []DNSRecord{record}) + require.NoError(t, err) + + records, err := client.GetDNSRecords(ctx, zone) + require.NoError(t, err) + + recordIdx, err := GetDNSRecordIdx(records, record) + require.NoError(t, err) + + assert.Equal(t, record.Hostname, records[recordIdx].Hostname) + assert.Equal(t, record.RecordType, records[recordIdx].RecordType) + assert.Equal(t, record.Destination, records[recordIdx].Destination) + assert.Equal(t, record.DeleteRecord, records[recordIdx].DeleteRecord) + + records[recordIdx].DeleteRecord = true + + // Tear down + err = client.UpdateDNSRecord(ctx, envTest.GetDomain(), []DNSRecord{records[recordIdx]}) + require.NoError(t, err, "Did not remove record! Please do so yourself.") + + err = client.Logout(ctx) + require.NoError(t, err) +} diff --git a/providers/dns/netcup/internal/fixtures/get_dns_records-request.json b/providers/dns/netcup/internal/fixtures/get_dns_records-request.json deleted file mode 100644 index bcf8e5310..000000000 --- a/providers/dns/netcup/internal/fixtures/get_dns_records-request.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "action": "infoDnsRecords", - "param": { - "domainname": "example.com", - "customernumber": "a", - "apikey": "b", - "apisessionid": "" - } -} diff --git a/providers/dns/netcup/internal/fixtures/get_dns_records.json b/providers/dns/netcup/internal/fixtures/get_dns_records.json deleted file mode 100644 index e521a8e24..000000000 --- a/providers/dns/netcup/internal/fixtures/get_dns_records.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "serverrequestid": "srv-request-id", - "clientrequestid": "", - "action": "infoDnsRecords", - "status": "success", - "statuscode": 2000, - "shortmessage": "Login successful", - "longmessage": "Session has been created successful.", - "responsedata": { - "apisessionid": "api-session-id", - "dnsrecords": [ - { - "id": "1", - "hostname": "example.com", - "type": "TXT", - "priority": "1", - "destination": "bGVnbzE=", - "state": "yes", - "ttl": 300 - }, - { - "id": "2", - "hostname": "example2.com", - "type": "TXT", - "priority": "1", - "destination": "bGVnbw==", - "state": "yes", - "ttl": 300 - } - ] - } -} diff --git a/providers/dns/netcup/internal/fixtures/get_dns_records_error.json b/providers/dns/netcup/internal/fixtures/get_dns_records_error.json deleted file mode 100644 index 3ba472366..000000000 --- a/providers/dns/netcup/internal/fixtures/get_dns_records_error.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "serverrequestid":"YxTr4EzdbJ101T211zR4yzUEMVE", - "clientrequestid":"", - "action":"infoDnsRecords", - "status":"error", - "statuscode":4013, - "shortmessage":"Validation Error.", - "longmessage":"Message is empty.", - "responsedata":"" -} diff --git a/providers/dns/netcup/internal/fixtures/get_dns_records_error_unmarshal.json b/providers/dns/netcup/internal/fixtures/get_dns_records_error_unmarshal.json deleted file mode 100644 index f8f91329f..000000000 --- a/providers/dns/netcup/internal/fixtures/get_dns_records_error_unmarshal.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "serverrequestid":"srv-request-id", - "clientrequestid":"", - "action":"infoDnsRecords", - "status":"success", - "statuscode":2000, - "shortmessage":"Login successful", - "longmessage":"Session has been created successful.", - "responsedata":"" -} diff --git a/providers/dns/netcup/internal/fixtures/login-request.json b/providers/dns/netcup/internal/fixtures/login-request.json deleted file mode 100644 index 1e287dfe0..000000000 --- a/providers/dns/netcup/internal/fixtures/login-request.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "action": "login", - "param": { - "customernumber": "a", - "apikey": "b", - "apipassword": "c" - } -} diff --git a/providers/dns/netcup/internal/fixtures/login.json b/providers/dns/netcup/internal/fixtures/login.json deleted file mode 100644 index a66979544..000000000 --- a/providers/dns/netcup/internal/fixtures/login.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "serverrequestid": "srv-request-id", - "clientrequestid": "", - "action": "login", - "status": "success", - "statuscode": 2000, - "shortmessage": "Login successful", - "longmessage": "Session has been created successful.", - "responsedata": { - "apisessionid": "api-session-id" - } -} diff --git a/providers/dns/netcup/internal/fixtures/login_error.json b/providers/dns/netcup/internal/fixtures/login_error.json deleted file mode 100644 index a32568f78..000000000 --- a/providers/dns/netcup/internal/fixtures/login_error.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "serverrequestid":"YxTr4EzdbJ101T211zR4yzUEMVE", - "clientrequestid":"", - "action":"login", - "status":"error", - "statuscode":4013, - "shortmessage":"Validation Error.", - "longmessage":"Message is empty.", - "responsedata":"" -} diff --git a/providers/dns/netcup/internal/fixtures/login_error_unmarshal.json b/providers/dns/netcup/internal/fixtures/login_error_unmarshal.json deleted file mode 100644 index 96e7cbd0c..000000000 --- a/providers/dns/netcup/internal/fixtures/login_error_unmarshal.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "serverrequestid": "srv-request-id", - "clientrequestid": "", - "action": "login", - "status": "success", - "statuscode": 2000, - "shortmessage": "Login successful", - "longmessage": "Session has been created successful.", - "responsedata": "" -} diff --git a/providers/dns/netcup/internal/fixtures/logout-request.json b/providers/dns/netcup/internal/fixtures/logout-request.json deleted file mode 100644 index add759c3a..000000000 --- a/providers/dns/netcup/internal/fixtures/logout-request.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "action": "logout", - "param": { - "customernumber": "a", - "apikey": "b", - "apisessionid": "session-id" - } -} diff --git a/providers/dns/netcup/internal/fixtures/logout.json b/providers/dns/netcup/internal/fixtures/logout.json deleted file mode 100644 index 50881fff3..000000000 --- a/providers/dns/netcup/internal/fixtures/logout.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "serverrequestid": "request-id", - "clientrequestid": "", - "action": "logout", - "status": "success", - "statuscode": 2000, - "shortmessage": "Logout successful", - "longmessage": "Session has been terminated successful.", - "responsedata": "" -} diff --git a/providers/dns/netcup/internal/fixtures/logout_error.json b/providers/dns/netcup/internal/fixtures/logout_error.json deleted file mode 100644 index a2de32da1..000000000 --- a/providers/dns/netcup/internal/fixtures/logout_error.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "serverrequestid":"YxTr4EzdbJ101T211zR4yzUEMVE", - "clientrequestid":"", - "action":"logout", - "status":"error", - "statuscode":4013, - "shortmessage":"Validation Error.", - "longmessage":"Message is empty.", - "responsedata":"" -} diff --git a/providers/dns/netcup/internal/session.go b/providers/dns/netcup/internal/session.go index b53751edf..6627d74e1 100644 --- a/providers/dns/netcup/internal/session.go +++ b/providers/dns/netcup/internal/session.go @@ -24,7 +24,6 @@ func (c *Client) login(ctx context.Context) (string, error) { } var responseData LoginResponse - err := c.doRequest(ctx, payload, &responseData) if err != nil { return "", fmt.Errorf("loging error: %w", err) diff --git a/providers/dns/netcup/internal/session_test.go b/providers/dns/netcup/internal/session_test.go index 7704c2604..2b69265d2 100644 --- a/providers/dns/netcup/internal/session_test.go +++ b/providers/dns/netcup/internal/session_test.go @@ -1,28 +1,59 @@ package internal import ( + "bytes" "context" + "fmt" + "io" "net/http" + "strconv" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockContext(t *testing.T) context.Context { - t.Helper() - - return context.WithValue(t.Context(), sessionIDKey, "session-id") +func mockContext() context.Context { + return context.WithValue(context.Background(), sessionIDKey, "session-id") } func TestClient_Login(t *testing.T) { - client := mockBuilder(). - Route("POST /", servermock.ResponseFromFixture("login.json"), - servermock.CheckRequestJSONBodyFromFixture("login-request.json")). - Build(t) + client, mux := setupTest(t) - sessionID, err := client.login(t.Context()) + mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { + raw, err := io.ReadAll(req.Body) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + if string(bytes.TrimSpace(raw)) != `{"action":"login","param":{"customernumber":"a","apikey":"b","apipassword":"c"}}` { + http.Error(rw, fmt.Sprintf("invalid request body: %s", string(raw)), http.StatusBadRequest) + return + } + + response := ` + { + "serverrequestid": "srv-request-id", + "clientrequestid": "", + "action": "login", + "status": "success", + "statuscode": 2000, + "shortmessage": "Login successful", + "longmessage": "Session has been created successful.", + "responsedata": { + "apisessionid": "api-session-id" + } + } + ` + _, err = rw.Write([]byte(response)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + sessionID, err := client.login(context.Background()) require.NoError(t, err) assert.Equal(t, "api-session-id", sessionID) @@ -30,24 +61,56 @@ func TestClient_Login(t *testing.T) { func TestClient_Login_errors(t *testing.T) { testCases := []struct { - desc string - handler http.Handler - expected string + desc string + handler func(rw http.ResponseWriter, req *http.Request) }{ { - desc: "HTTP error", - handler: servermock.Noop().WithStatusCode(http.StatusInternalServerError), - expected: `loging error: unexpected status code: [status code: 500] body: `, + desc: "HTTP error", + handler: func(rw http.ResponseWriter, _ *http.Request) { + http.Error(rw, "error message", http.StatusInternalServerError) + }, }, { - desc: "API error", - handler: servermock.ResponseFromFixture("login_error.json"), - expected: `loging error: an error occurred during the action login: [Status=error, StatusCode=4013, ShortMessage=Validation Error., LongMessage=Message is empty.]`, + desc: "API error", + handler: func(rw http.ResponseWriter, _ *http.Request) { + response := ` + { + "serverrequestid":"YxTr4EzdbJ101T211zR4yzUEMVE", + "clientrequestid":"", + "action":"login", + "status":"error", + "statuscode":4013, + "shortmessage":"Validation Error.", + "longmessage":"Message is empty.", + "responsedata":"" + }` + _, err := rw.Write([]byte(response)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }, }, { - desc: "responsedata marshaling error", - handler: servermock.ResponseFromFixture("login_error_unmarshal.json"), - expected: `loging error: unable to unmarshal response: [status code: 200] body: "" error: json: cannot unmarshal string into Go value of type internal.LoginResponse`, + desc: "responsedata marshaling error", + handler: func(rw http.ResponseWriter, _ *http.Request) { + response := ` + { + "serverrequestid": "srv-request-id", + "clientrequestid": "", + "action": "login", + "status": "success", + "statuscode": 2000, + "shortmessage": "Login successful", + "longmessage": "Session has been created successful.", + "responsedata": "" + }` + _, err := rw.Write([]byte(response)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }, }, } @@ -55,40 +118,85 @@ func TestClient_Login_errors(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - client := mockBuilder(). - Route("POST /", test.handler). - Build(t) + client, mux := setupTest(t) - sessionID, err := client.login(t.Context()) - assert.EqualError(t, err, test.expected) - assert.Empty(t, sessionID) + mux.HandleFunc("/", test.handler) + + sessionID, err := client.login(context.Background()) + assert.Error(t, err) + assert.Equal(t, "", sessionID) }) } } func TestClient_Logout(t *testing.T) { - client := mockBuilder(). - Route("POST /", servermock.ResponseFromFixture("logout.json"), - servermock.CheckRequestJSONBodyFromFixture("logout-request.json")). - Build(t) + client, mux := setupTest(t) - err := client.Logout(mockContext(t)) + mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { + raw, err := io.ReadAll(req.Body) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + if string(bytes.TrimSpace(raw)) != `{"action":"logout","param":{"customernumber":"a","apikey":"b","apisessionid":"session-id"}}` { + http.Error(rw, fmt.Sprintf("invalid request body: %s", string(raw)), http.StatusBadRequest) + return + } + + response := ` + { + "serverrequestid": "request-id", + "clientrequestid": "", + "action": "logout", + "status": "success", + "statuscode": 2000, + "shortmessage": "Logout successful", + "longmessage": "Session has been terminated successful.", + "responsedata": "" + }` + _, err = rw.Write([]byte(response)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + err := client.Logout(mockContext()) require.NoError(t, err) } func TestClient_Logout_errors(t *testing.T) { testCases := []struct { - desc string - handler http.Handler - expected string + desc string + handler func(rw http.ResponseWriter, req *http.Request) }{ { - desc: "HTTP error", - handler: servermock.Noop().WithStatusCode(http.StatusInternalServerError), + desc: "HTTP error", + handler: func(rw http.ResponseWriter, _ *http.Request) { + http.Error(rw, "error message", http.StatusInternalServerError) + }, }, { - desc: "API error", - handler: servermock.ResponseFromFixture("login_error.json"), + desc: "API error", + handler: func(rw http.ResponseWriter, _ *http.Request) { + response := ` + { + "serverrequestid":"YxTr4EzdbJ101T211zR4yzUEMVE", + "clientrequestid":"", + "action":"logout", + "status":"error", + "statuscode":4013, + "shortmessage":"Validation Error.", + "longmessage":"Message is empty.", + "responsedata":"" + }` + _, err := rw.Write([]byte(response)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }, }, } @@ -96,12 +204,39 @@ func TestClient_Logout_errors(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - client := mockBuilder(). - Route("POST /", test.handler). - Build(t) + client, mux := setupTest(t) - err := client.Logout(t.Context()) + mux.HandleFunc("/", test.handler) + + err := client.Logout(context.Background()) require.Error(t, err) }) } } + +func TestLiveClientAuth(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + // Setup + envTest.RestoreEnv() + + client, err := NewClient( + envTest.GetValue("NETCUP_CUSTOMER_NUMBER"), + envTest.GetValue("NETCUP_API_KEY"), + envTest.GetValue("NETCUP_API_PASSWORD")) + require.NoError(t, err) + + for i := range 4 { + t.Run("Test_"+strconv.Itoa(i+1), func(t *testing.T) { + t.Parallel() + + ctx, err := client.CreateSessionContext(context.Background()) + require.NoError(t, err) + + err = client.Logout(ctx) + require.NoError(t, err) + }) + } +} diff --git a/providers/dns/netcup/internal/types.go b/providers/dns/netcup/internal/types.go index e4cc5ec14..55212f909 100644 --- a/providers/dns/netcup/internal/types.go +++ b/providers/dns/netcup/internal/types.go @@ -72,6 +72,7 @@ type DNSRecord struct { Destination string `json:"destination"` DeleteRecord bool `json:"deleterecord,omitempty"` State string `json:"state,omitempty"` + TTL int `json:"ttl,omitempty"` } // ResponseMsg as specified in netcup WSDL. diff --git a/providers/dns/netcup/netcup.go b/providers/dns/netcup/netcup.go index 13b329e07..a8fc8b172 100644 --- a/providers/dns/netcup/netcup.go +++ b/providers/dns/netcup/netcup.go @@ -9,11 +9,9 @@ import ( "strings" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/netcup/internal" ) @@ -25,34 +23,29 @@ const ( EnvAPIKey = envNamespace + "API_KEY" EnvAPIPassword = envNamespace + "API_PASSWORD" - // Deprecated: the TTL is not configurable on record. - EnvTTL = envNamespace + "TTL" - + EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Key string Password string Customer string + TTL int PropagationTimeout time.Duration PollingInterval time.Duration HTTPClient *http.Client - - // Deprecated: the TTL is not configurable on record. - TTL int } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 15*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 30*time.Second), + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), }, @@ -93,11 +86,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, fmt.Errorf("netcup: %w", err) } - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + client.HTTPClient = config.HTTPClient return &DNSProvider{client: client, config: config}, nil } @@ -119,7 +108,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { defer func() { err = d.client.Logout(ctx) if err != nil { - log.Printf("netcup: %v", err) + log.Print("netcup: %v", err) } }() @@ -128,6 +117,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { Hostname: hostname, RecordType: "TXT", Destination: info.Value, + TTL: d.config.TTL, } zone = dns01.UnFqdn(zone) @@ -165,7 +155,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { defer func() { err = d.client.Logout(ctx) if err != nil { - log.Printf("netcup: %v", err) + log.Print("netcup: %v", err) } }() diff --git a/providers/dns/netcup/netcup.toml b/providers/dns/netcup/netcup.toml index 4ef8688c6..790d97ba0 100644 --- a/providers/dns/netcup/netcup.toml +++ b/providers/dns/netcup/netcup.toml @@ -8,7 +8,7 @@ Example = ''' NETCUP_CUSTOMER_NUMBER=xxxx \ NETCUP_API_KEY=yyyy \ NETCUP_API_PASSWORD=zzzz \ -lego --dns netcup -d '*.example.com' -d example.com run +lego --email you@example.com --dns netcup --domains my.example.org run ''' [Configuration] @@ -17,9 +17,10 @@ lego --dns netcup -d '*.example.com' -d example.com run NETCUP_API_KEY = "API key" NETCUP_API_PASSWORD = "API password" [Configuration.Additional] - NETCUP_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 30)" - NETCUP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 900)" - NETCUP_HTTP_TIMEOUT = "API request timeout in seconds (Default: 10)" + NETCUP_POLLING_INTERVAL = "Time between DNS propagation check" + NETCUP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + NETCUP_TTL = "The TTL of the TXT record used for the DNS challenge" + NETCUP_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.netcup-wiki.de/wiki/DNS_API" diff --git a/providers/dns/netcup/netcup_test.go b/providers/dns/netcup/netcup_test.go index fedc56ba9..f9cc43ab9 100644 --- a/providers/dns/netcup/netcup_test.go +++ b/providers/dns/netcup/netcup_test.go @@ -72,7 +72,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -159,14 +158,13 @@ func TestLivePresentAndCleanup(t *testing.T) { } envTest.RestoreEnv() - p, err := NewDNSProvider() require.NoError(t, err) info := dns01.GetChallengeInfo(envTest.GetDomain(), "123d==") zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - require.NoError(t, err) + require.NoError(t, err, "error finding DNSZone") zone = dns01.UnFqdn(zone) @@ -183,7 +181,7 @@ func TestLivePresentAndCleanup(t *testing.T) { require.NoError(t, err) err = p.CleanUp(test, "987d", "123d==") - require.NoError(t, err) + require.NoError(t, err, "Did not clean up! Please remove record yourself.") }) } } diff --git a/providers/dns/netlify/internal/client.go b/providers/dns/netlify/internal/client.go index 3b6b681fe..06651bdec 100644 --- a/providers/dns/netlify/internal/client.go +++ b/providers/dns/netlify/internal/client.go @@ -59,7 +59,6 @@ func (c *Client) GetRecords(ctx context.Context, zoneID string) ([]DNSRecord, er } var records []DNSRecord - err = json.Unmarshal(raw, &records) if err != nil { return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) @@ -94,7 +93,6 @@ func (c *Client) CreateRecord(ctx context.Context, zoneID string, record DNSReco } var recordResp DNSRecord - err = json.Unmarshal(raw, &recordResp) if err != nil { return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) @@ -126,7 +124,7 @@ func (c *Client) RemoveRecord(ctx context.Context, zoneID, recordID string) erro return nil } -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload interface{}) (*http.Request, error) { buf := new(bytes.Buffer) if payload != nil { diff --git a/providers/dns/netlify/internal/client_test.go b/providers/dns/netlify/internal/client_test.go index b19a8f071..e06a579b7 100644 --- a/providers/dns/netlify/internal/client_test.go +++ b/providers/dns/netlify/internal/client_test.go @@ -1,35 +1,64 @@ package internal import ( + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func setupClient(token string) func(server *httptest.Server) (*Client, error) { - return func(server *httptest.Server) (*Client, error) { - client := NewClient(OAuthStaticAccessToken(server.Client(), token)) - client.baseURL, _ = url.Parse(server.URL) +func setupTest(t *testing.T, token string) (*Client, *http.ServeMux) { + t.Helper() - return client, nil - } + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient(OAuthStaticAccessToken(server.Client(), token)) + client.baseURL, _ = url.Parse(server.URL) + + return client, mux } func TestClient_GetRecords(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient("tokenA"), - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("Bearer tokenA"), - ). - Route("GET /dns_zones/zoneID/dns_records", - servermock.ResponseFromFixture("get_records.json")). - Build(t) + client, mux := setupTest(t, "tokenA") - records, err := client.GetRecords(t.Context(), "zoneID") + mux.HandleFunc("/dns_zones/zoneID/dns_records", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, "unsupported method", http.StatusMethodNotAllowed) + return + } + + auth := req.Header.Get("Authorization") + if auth != "Bearer tokenA" { + http.Error(rw, fmt.Sprintf("invali token: %s", auth), http.StatusUnauthorized) + return + } + + rw.Header().Set("Content-Type", "application/json; charset=utf-8") + + file, err := os.Open("./fixtures/get_records.json") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + records, err := client.GetRecords(context.Background(), "zoneID") require.NoError(t, err) expected := []DNSRecord{ @@ -41,16 +70,36 @@ func TestClient_GetRecords(t *testing.T) { } func TestClient_CreateRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient("tokenB"), - servermock.CheckHeader(). - WithAccept("application/json"). - WithContentType("application/json; charset=utf-8"). - WithAuthorization("Bearer tokenB"), - ). - Route("POST /dns_zones/zoneID/dns_records", - servermock.ResponseFromFixture("create_record.json"). - WithStatusCode(http.StatusCreated)). - Build(t) + client, mux := setupTest(t, "tokenB") + + mux.HandleFunc("/dns_zones/zoneID/dns_records", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, "unsupported method", http.StatusMethodNotAllowed) + return + } + + auth := req.Header.Get("Authorization") + if auth != "Bearer tokenB" { + http.Error(rw, fmt.Sprintf("invali token: %s", auth), http.StatusUnauthorized) + return + } + + rw.Header().Set("Content-Type", "application/json; charset=utf-8") + rw.WriteHeader(http.StatusCreated) + + file, err := os.Open("./fixtures/create_record.json") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) record := DNSRecord{ Hostname: "_acme-challenge.example.com", @@ -59,7 +108,7 @@ func TestClient_CreateRecord(t *testing.T) { Value: "txtxtxtxtxtxt", } - result, err := client.CreateRecord(t.Context(), "zoneID", record) + result, err := client.CreateRecord(context.Background(), "zoneID", record) require.NoError(t, err) expected := &DNSRecord{ @@ -74,15 +123,23 @@ func TestClient_CreateRecord(t *testing.T) { } func TestClient_RemoveRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient("tokenC"), - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("Bearer tokenC"), - ). - Route("DELETE /dns_zones/zoneID/dns_records/recordID", - servermock.Noop(). - WithStatusCode(http.StatusNoContent)). - Build(t) + client, mux := setupTest(t, "tokenC") - err := client.RemoveRecord(t.Context(), "zoneID", "recordID") + mux.HandleFunc("/dns_zones/zoneID/dns_records/recordID", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodDelete { + http.Error(rw, "unsupported method", http.StatusMethodNotAllowed) + return + } + + auth := req.Header.Get("Authorization") + if auth != "Bearer tokenC" { + http.Error(rw, fmt.Sprintf("invali token: %s", auth), http.StatusUnauthorized) + return + } + + rw.WriteHeader(http.StatusNoContent) + }) + + err := client.RemoveRecord(context.Background(), "zoneID", "recordID") require.NoError(t, err) } diff --git a/providers/dns/netlify/netlify.go b/providers/dns/netlify/netlify.go index 5b2980d24..1a65e330d 100644 --- a/providers/dns/netlify/netlify.go +++ b/providers/dns/netlify/netlify.go @@ -10,10 +10,8 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/netlify/internal" ) @@ -29,8 +27,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Token string @@ -85,11 +81,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("netlify: incomplete credentials, missing token") } - client := internal.NewClient( - clientdebug.Wrap( - internal.OAuthStaticAccessToken(config.HTTPClient, config.Token), - ), - ) + client := internal.NewClient(internal.OAuthStaticAccessToken(config.HTTPClient, config.Token)) return &DNSProvider{ config: config, @@ -149,7 +141,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() - if !ok { return fmt.Errorf("netlify: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) } diff --git a/providers/dns/netlify/netlify.toml b/providers/dns/netlify/netlify.toml index 9d3c0f6b5..af53c7b29 100644 --- a/providers/dns/netlify/netlify.toml +++ b/providers/dns/netlify/netlify.toml @@ -6,17 +6,17 @@ Since = "v3.7.0" Example = ''' NETLIFY_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --dns netlify -d '*.example.com' -d example.com run +lego --email you@example.com --dns netlify --domains my.example.org run ''' [Configuration] [Configuration.Credentials] NETLIFY_TOKEN = "Token" [Configuration.Additional] - NETLIFY_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - NETLIFY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - NETLIFY_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - NETLIFY_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + NETLIFY_POLLING_INTERVAL = "Time between DNS propagation check" + NETLIFY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + NETLIFY_TTL = "The TTL of the TXT record used for the DNS challenge" + NETLIFY_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://open-api.netlify.com/" diff --git a/providers/dns/netlify/netlify_test.go b/providers/dns/netlify/netlify_test.go index 1e84517be..f351802da 100644 --- a/providers/dns/netlify/netlify_test.go +++ b/providers/dns/netlify/netlify_test.go @@ -36,7 +36,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -94,7 +93,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -108,7 +106,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/nicmanager/internal/client.go b/providers/dns/nicmanager/internal/client.go index 16bfe497b..3134fc4fd 100644 --- a/providers/dns/nicmanager/internal/client.go +++ b/providers/dns/nicmanager/internal/client.go @@ -23,7 +23,7 @@ const ( // Modes. const ( ModeAnycast = "anycast" - ModeZone = "zones" + ModeZone = "zone" ) // Options the Client options. @@ -74,7 +74,7 @@ func NewClient(opts Options) *Client { return c } -func (c *Client) GetZone(ctx context.Context, name string) (*Zone, error) { +func (c Client) GetZone(ctx context.Context, name string) (*Zone, error) { endpoint := c.baseURL.JoinPath(c.mode, name) req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) @@ -83,7 +83,6 @@ func (c *Client) GetZone(ctx context.Context, name string) (*Zone, error) { } var zone Zone - err = c.do(req, http.StatusOK, &zone) if err != nil { return nil, err @@ -92,7 +91,7 @@ func (c *Client) GetZone(ctx context.Context, name string) (*Zone, error) { return &zone, nil } -func (c *Client) AddRecord(ctx context.Context, zone string, payload RecordCreateUpdate) error { +func (c Client) AddRecord(ctx context.Context, zone string, payload RecordCreateUpdate) error { endpoint := c.baseURL.JoinPath(c.mode, zone, "records") req, err := newJSONRequest(ctx, http.MethodPost, endpoint, payload) @@ -108,7 +107,7 @@ func (c *Client) AddRecord(ctx context.Context, zone string, payload RecordCreat return nil } -func (c *Client) DeleteRecord(ctx context.Context, zone string, record int) error { +func (c Client) DeleteRecord(ctx context.Context, zone string, record int) error { endpoint := c.baseURL.JoinPath(c.mode, zone, "records", strconv.Itoa(record)) req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) @@ -124,7 +123,7 @@ func (c *Client) DeleteRecord(ctx context.Context, zone string, record int) erro return nil } -func (c *Client) do(req *http.Request, expectedStatusCode int, result any) error { +func (c Client) do(req *http.Request, expectedStatusCode int, result any) error { req.SetBasicAuth(c.username, c.password) if c.otp != "" { diff --git a/providers/dns/nicmanager/internal/client_test.go b/providers/dns/nicmanager/internal/client_test.go index 1eb7d5a36..822ec0db2 100644 --- a/providers/dns/nicmanager/internal/client_test.go +++ b/providers/dns/nicmanager/internal/client_test.go @@ -1,44 +1,24 @@ package internal import ( + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - opts := Options{ - Login: "l", - Username: "u", - Password: "p", - OTP: "2hsn", - } - - client := NewClient(opts) - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithBasicAuth("l.u", "p"). - WithRegexp(headerTOTPToken, `\d{6}`)) -} - func TestClient_GetZone(t *testing.T) { - client := mockBuilder(). - Route("GET /anycast/nicmanager-anycastdns4.net", - servermock.ResponseFromFixture("zone.json")). - Build(t) + client := setupTest(t, "/anycast/nicmanager-anycastdns4.net", testHandler(http.MethodGet, http.StatusOK, "zone.json")) - zone, err := client.GetZone(t.Context(), "nicmanager-anycastdns4.net") + zone, err := client.GetZone(context.Background(), "nicmanager-anycastdns4.net") require.NoError(t, err) expected := &Zone{ @@ -59,22 +39,14 @@ func TestClient_GetZone(t *testing.T) { } func TestClient_GetZone_error(t *testing.T) { - client := mockBuilder(). - Route("GET /anycast/foo", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusNotFound)). - Build(t) + client := setupTest(t, "/anycast/foo", testHandler(http.MethodGet, http.StatusNotFound, "error.json")) - _, err := client.GetZone(t.Context(), "foo") - require.EqualError(t, err, "404: Not Found") + _, err := client.GetZone(context.Background(), "foo") + require.Error(t, err) } func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /anycast/zonedomain.tld/records", - servermock.Noop(). - WithStatusCode(http.StatusAccepted)). - Build(t) + client := setupTest(t, "/anycast/zonedomain.tld/records", testHandler(http.MethodPost, http.StatusAccepted, "error.json")) record := RecordCreateUpdate{ Type: "TXT", @@ -83,16 +55,12 @@ func TestClient_AddRecord(t *testing.T) { TTL: 3600, } - err := client.AddRecord(t.Context(), "zonedomain.tld", record) + err := client.AddRecord(context.Background(), "zonedomain.tld", record) require.NoError(t, err) } func TestClient_AddRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /anycast/zonedomain.tld/records", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client := setupTest(t, "/anycast/zonedomain.tld", testHandler(http.MethodPost, http.StatusUnauthorized, "error.json")) record := RecordCreateUpdate{ Type: "TXT", @@ -101,28 +69,78 @@ func TestClient_AddRecord_error(t *testing.T) { TTL: 3600, } - err := client.AddRecord(t.Context(), "zonedomain.tld", record) - require.EqualError(t, err, "401: Not Found") + err := client.AddRecord(context.Background(), "zonedomain.tld", record) + require.Error(t, err) } func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /anycast/zonedomain.tld/records/6", - servermock.Noop(). - WithStatusCode(http.StatusAccepted)). - Build(t) + client := setupTest(t, "/anycast/zonedomain.tld/records/6", testHandler(http.MethodDelete, http.StatusAccepted, "error.json")) - err := client.DeleteRecord(t.Context(), "zonedomain.tld", 6) + err := client.DeleteRecord(context.Background(), "zonedomain.tld", 6) require.NoError(t, err) } func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /anycast/zonedomain.tld/records/6", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusNotFound)). - Build(t) + client := setupTest(t, "/anycast/zonedomain.tld/records/6", testHandler(http.MethodDelete, http.StatusNoContent, "")) - err := client.DeleteRecord(t.Context(), "zonedomain.tld", 6) - require.EqualError(t, err, "404: Not Found") + err := client.DeleteRecord(context.Background(), "zonedomain.tld", 7) + require.Error(t, err) +} + +func setupTest(t *testing.T, path string, handler http.Handler) *Client { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.Handle(path, handler) + + opts := Options{ + Login: "foo", + Username: "bar", + Password: "foo", + OTP: "2hsn", + } + + client := NewClient(opts) + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client +} + +func testHandler(method string, statusCode int, filename string) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed) + return + } + + username, password, ok := req.BasicAuth() + if !ok || username != "foo.bar" || password != "foo" { + http.Error(rw, `{"message":"Unauthenticated"}`, http.StatusUnauthorized) + return + } + + rw.WriteHeader(statusCode) + + if statusCode == http.StatusNoContent { + return + } + + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError) + return + } + + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError) + return + } + } } diff --git a/providers/dns/nicmanager/nicmanager.go b/providers/dns/nicmanager/nicmanager.go index 9b27df64e..5f7eaff60 100644 --- a/providers/dns/nicmanager/nicmanager.go +++ b/providers/dns/nicmanager/nicmanager.go @@ -9,10 +9,8 @@ import ( "strings" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/nicmanager/internal" ) @@ -25,7 +23,7 @@ const ( EnvEmail = envNamespace + "API_EMAIL" EnvPassword = envNamespace + "API_PASSWORD" EnvOTP = envNamespace + "API_OTP" - EnvMode = envNamespace + "API_MODE" + EnvMode = envNamespace + "MODE" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" @@ -35,8 +33,6 @@ const ( const minTTL = 900 -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Login string @@ -86,7 +82,7 @@ func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() config.Password = values[EnvPassword] - config.Mode = env.GetOneWithFallback(EnvMode, internal.ModeAnycast, env.ParseString, envNamespace+"MODE") + config.Mode = env.GetOrDefaultString(EnvMode, internal.ModeAnycast) config.Username = env.GetOrFile(EnvUsername) config.Login = env.GetOrFile(EnvLogin) config.Email = env.GetOrFile(EnvEmail) @@ -129,8 +125,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{client: client, config: config}, nil } @@ -191,11 +185,8 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { name := dns01.UnFqdn(info.EffectiveFQDN) - var ( - existingRecord internal.Record - existingRecordFound bool - ) - + var existingRecord internal.Record + var existingRecordFound bool for _, record := range zone.Records { if strings.EqualFold(record.Type, "TXT") && strings.EqualFold(record.Name, name) && record.Content == info.Value { existingRecord = record diff --git a/providers/dns/nicmanager/nicmanager.toml b/providers/dns/nicmanager/nicmanager.toml index d5921de5a..913f685b4 100644 --- a/providers/dns/nicmanager/nicmanager.toml +++ b/providers/dns/nicmanager/nicmanager.toml @@ -13,7 +13,7 @@ NICMANAGER_API_PASSWORD = "password" \ # Optionally, if your account has TOTP enabled, set the secret here NICMANAGER_API_OTP = "long-secret" \ -lego --dns nicmanager -d '*.example.com' -d example.com run +lego --email you@example.com --dns nicmanager --domains my.example.org run ## Login using account name + username @@ -24,14 +24,14 @@ NICMANAGER_API_PASSWORD = "password" \ # Optionally, if your account has TOTP enabled, set the secret here NICMANAGER_API_OTP = "long-secret" \ -lego --dns nicmanager -d '*.example.com' -d example.com run +lego --email you@example.com --dns nicmanager --domains my.example.org run ''' Additional = ''' ## Description You can log in using your account name + username or using your email address. -Optionally, if TOTP is configured for your account, set `NICMANAGER_API_OTP`. +Optionally if TOTP is configured for your account, set `NICMANAGER_API_OTP`. ''' [Configuration] @@ -42,11 +42,11 @@ Optionally, if TOTP is configured for your account, set `NICMANAGER_API_OTP`. NICMANAGER_API_PASSWORD = "Password, always required" [Configuration.Additional] NICMANAGER_API_OTP = "TOTP Secret (optional)" - NICMANAGER_API_MODE = "mode: 'anycast' or 'zones' (for FreeDNS) (default: 'anycast')" - NICMANAGER_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - NICMANAGER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 300)" - NICMANAGER_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 900)" - NICMANAGER_HTTP_TIMEOUT = "API request timeout in seconds (Default: 10)" + NICMANAGER_API_MODE = "mode: 'anycast' or 'zone' (default: 'anycast')" + NICMANAGER_POLLING_INTERVAL = "Time between DNS propagation check" + NICMANAGER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + NICMANAGER_TTL = "The TTL of the TXT record used for the DNS challenge" + NICMANAGER_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://api.nicmanager.com/docs/v1/" diff --git a/providers/dns/nicmanager/nicmanager_test.go b/providers/dns/nicmanager/nicmanager_test.go index 114cdb7ca..bc2f50cc3 100644 --- a/providers/dns/nicmanager/nicmanager_test.go +++ b/providers/dns/nicmanager/nicmanager_test.go @@ -66,7 +66,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -160,7 +159,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -174,7 +172,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/nicru/internal/client.go b/providers/dns/nicru/internal/client.go deleted file mode 100644 index 5d851fc76..000000000 --- a/providers/dns/nicru/internal/client.go +++ /dev/null @@ -1,250 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/xml" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" -) - -const ( - apiBaseURL = "https://api.nic.ru/dns-master" - tokenURL = "https://api.nic.ru/oauth/token" -) - -const successStatus = "success" - -// Trimmer trim all XML fields. -type Trimmer struct { - decoder *xml.Decoder -} - -func (tr Trimmer) Token() (xml.Token, error) { - t, err := tr.decoder.Token() - if cd, ok := t.(xml.CharData); ok { - t = xml.CharData(bytes.TrimSpace(cd)) - } - - return t, err -} - -type Client struct { - baseURL *url.URL - httpClient *http.Client -} - -func NewClient(httpClient *http.Client) (*Client, error) { - if httpClient == nil { - httpClient = &http.Client{Timeout: 5 * time.Second} - } - - baseURL, _ := url.Parse(apiBaseURL) - - return &Client{ - baseURL: baseURL, - httpClient: httpClient, - }, nil -} - -func (c *Client) GetServices(ctx context.Context) ([]Service, error) { - endpoint := c.baseURL.JoinPath("services") - - req, err := newXMLRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - apiResponse, err := c.do(req) - if err != nil { - return nil, err - } - - if apiResponse.Data == nil { - return nil, nil - } - - return apiResponse.Data.Service, nil -} - -func (c *Client) ListZones(ctx context.Context) ([]Zone, error) { - endpoint := c.baseURL.JoinPath("zones") - - req, err := newXMLRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - apiResponse, err := c.do(req) - if err != nil { - return nil, err - } - - if apiResponse.Data == nil { - return nil, nil - } - - return apiResponse.Data.Zone, nil -} - -func (c *Client) GetZonesByService(ctx context.Context, serviceName string) ([]Zone, error) { - endpoint := c.baseURL.JoinPath("services", serviceName, "zones") - - req, err := newXMLRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - apiResponse, err := c.do(req) - if err != nil { - return nil, err - } - - if apiResponse.Data == nil { - return nil, nil - } - - return apiResponse.Data.Zone, nil -} - -func (c *Client) GetRecords(ctx context.Context, serviceName, zoneName string) ([]RR, error) { - endpoint := c.baseURL.JoinPath("services", serviceName, "zones", zoneName, "records") - - req, err := newXMLRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - apiResponse, err := c.do(req) - if err != nil { - return nil, err - } - - if apiResponse.Data == nil { - return nil, nil - } - - var records []RR - for _, zone := range apiResponse.Data.Zone { - records = append(records, zone.RR...) - } - - return records, nil -} - -func (c *Client) DeleteRecord(ctx context.Context, serviceName, zoneName, id string) error { - endpoint := c.baseURL.JoinPath("services", serviceName, "zones", zoneName, "records", id) - - req, err := newXMLRequest(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return err - } - - _, err = c.do(req) - if err != nil { - return err - } - - return nil -} - -func (c *Client) CommitZone(ctx context.Context, serviceName, zoneName string) error { - endpoint := c.baseURL.JoinPath("services", serviceName, "zones", zoneName, "commit") - - req, err := newXMLRequest(ctx, http.MethodPost, endpoint, nil) - if err != nil { - return err - } - - _, err = c.do(req) - if err != nil { - return err - } - - return nil -} - -func (c *Client) AddRecords(ctx context.Context, serviceName, zoneName string, rrs []RR) ([]Zone, error) { - endpoint := c.baseURL.JoinPath("services", serviceName, "zones", zoneName, "records") - - payload := &Request{RRList: &RRList{RR: rrs}} - - req, err := newXMLRequest(ctx, http.MethodPut, endpoint, payload) - if err != nil { - return nil, err - } - - apiResponse, err := c.do(req) - if err != nil { - return nil, err - } - - if apiResponse.Data == nil { - return nil, nil - } - - return apiResponse.Data.Zone, nil -} - -func (c *Client) do(req *http.Request) (*Response, error) { - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - apiResponse := &Response{} - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return nil, errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - decoder := xml.NewTokenDecoder(Trimmer{decoder: xml.NewDecoder(bytes.NewReader(raw))}) - - err = decoder.Decode(apiResponse) - if err != nil { - return nil, fmt.Errorf("[status code=%d] decode XML response: %s", resp.StatusCode, string(raw)) - } - - if apiResponse.Status != successStatus { - return nil, fmt.Errorf("[status code=%d] %s: %w", resp.StatusCode, apiResponse.Status, apiResponse.Errors.Error) - } - - return apiResponse, nil -} - -func newXMLRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - body := new(bytes.Buffer) - - if payload != nil { - body.WriteString(xml.Header) - - encoder := xml.NewEncoder(body) - encoder.Indent("", " ") - - err := encoder.Encode(payload) - if err != nil { - return nil, err - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "text/xml") - - if payload != nil { - req.Header.Set("Content-Type", "text/xml") - } - - return req, nil -} diff --git a/providers/dns/nicru/internal/client_test.go b/providers/dns/nicru/internal/client_test.go deleted file mode 100644 index f01300406..000000000 --- a/providers/dns/nicru/internal/client_test.go +++ /dev/null @@ -1,398 +0,0 @@ -package internal - -import ( - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient(server.Client()) - if err != nil { - return nil, err - } - - client.baseURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckHeader(). - WithAccept("text/xml"), - ) -} - -func TestClient_GetServices(t *testing.T) { - client := mockBuilder(). - Route("GET /services", servermock.ResponseFromFixture("services_GET.xml")). - Build(t) - - zones, err := client.GetServices(t.Context()) - require.NoError(t, err) - - expected := []Service{ - { - Admin: "123/NIC-REG", - DomainsLimit: "12", - DomainsNum: "5", - Enable: "true", - HasPrimary: "false", - Name: "testservice", - Payer: "123/NIC-REG", - Tariff: "Secondary L", - }, - { - Admin: "123/NIC-REG", - DomainsLimit: "150", - DomainsNum: "10", - Enable: "true", - HasPrimary: "true", - Name: "myservice", - Payer: "123/NIC-REG", - Tariff: "DNS-master XXL", - RRLimit: "7500", - RRNum: "1000", - }, - } - - assert.Equal(t, expected, zones) -} - -func TestClient_ListZones(t *testing.T) { - client := mockBuilder(). - Route("GET /zones", servermock.ResponseFromFixture("zones_all_GET.xml")). - Build(t) - - zones, err := client.ListZones(t.Context()) - require.NoError(t, err) - - expected := []Zone{ - { - Admin: "123/NIC-REG", - Enable: "true", - HasChanges: "false", - HasPrimary: "true", - ID: "227645", - IDNName: "тест.рф", - Name: "xn—e1aybc.xn--p1ai", - Payer: "123/NIC-REG", - Service: "myservice", - }, - { - Admin: "123/NIC-REG", - Enable: "true", - HasChanges: "false", - HasPrimary: "true", - ID: "227642", - IDNName: "example.ru", - Name: "example.ru", - Payer: "123/NIC-REG", - Service: "myservice", - }, - { - Admin: "123/NIC-REG", - Enable: "true", - HasChanges: "false", - HasPrimary: "true", - ID: "227643", - IDNName: "test.su", - Name: "test.su", - Payer: "123/NIC-REG", - Service: "myservice", - }, - } - - assert.Equal(t, expected, zones) -} - -func TestClient_ListZones_error(t *testing.T) { - client := mockBuilder(). - Route("GET /zones", servermock.ResponseFromFixture("errors.xml")). - Build(t) - - _, err := client.ListZones(t.Context()) - require.ErrorIs(t, err, Error{ - Text: "Access token expired or not found", - Code: "4097", - }) -} - -func TestClient_GetZonesByService(t *testing.T) { - client := mockBuilder(). - Route("GET /services/test/zones", - servermock.ResponseFromFixture("zones_GET.xml")). - Build(t) - - zones, err := client.GetZonesByService(t.Context(), "test") - require.NoError(t, err) - - expected := []Zone{ - { - Admin: "123/NIC-REG", - Enable: "true", - HasChanges: "false", - HasPrimary: "true", - ID: "227645", - IDNName: "тест.рф", - Name: "xn—e1aybc.xn--p1ai", - Payer: "123/NIC-REG", - Service: "myservice", - }, - { - Admin: "123/NIC-REG", - Enable: "true", - HasChanges: "false", - HasPrimary: "true", - ID: "227642", - IDNName: "example.ru", - Name: "example.ru", - Payer: "123/NIC-REG", - Service: "myservice", - }, - { - Admin: "123/NIC-REG", - Enable: "true", - HasChanges: "false", - HasPrimary: "true", - ID: "227643", - IDNName: "test.su", - Name: "test.su", - Payer: "123/NIC-REG", - Service: "myservice", - }, - } - - assert.Equal(t, expected, zones) -} - -func TestClient_GetZonesByService_error(t *testing.T) { - client := mockBuilder(). - Route("GET /services/test/zones", - servermock.ResponseFromFixture("errors.xml")). - Build(t) - - _, err := client.GetZonesByService(t.Context(), "test") - require.ErrorIs(t, err, Error{ - Text: "Access token expired or not found", - Code: "4097", - }) -} - -func TestClient_GetRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /services/test/zones/example.com./records", - servermock.ResponseFromFixture("records_GET.xml")). - Build(t) - - records, err := client.GetRecords(t.Context(), "test", "example.com.") - require.NoError(t, err) - - expected := []RR{ - { - ID: "210074", - Name: "@", - IDNName: "@", - TTL: "", - Type: "SOA", - SOA: &SOA{ - MName: &MName{ - Name: "ns3-l2.nic.ru.", - IDNName: "ns3-l2.nic.ru.", - }, - RName: &RName{ - Name: "dns.nic.ru.", - IDNName: "dns.nic.ru.", - }, - Serial: "2011112002", - Refresh: "1440", - Retry: "3600", - Expire: "2592000", - Minimum: "600", - }, - }, - { - ID: "210075", - Name: "@", - IDNName: "@", - Type: "NS", - NS: &NS{ - Name: "ns3-l2.nic.ru.", - IDNName: "ns3- l2.nic.ru.", - }, - }, - { - ID: "210076", - Name: "@", - IDNName: "@", - Type: "NS", - NS: &NS{ - Name: "ns4-l2.nic.ru.", - IDNName: "ns4-l2.nic.ru.", - }, - }, - { - ID: "210077", - Name: "@", - IDNName: "@", - Type: "NS", - NS: &NS{ - Name: "ns8-l2.nic.ru.", - IDNName: "ns8- l2.nic.ru.", - }, - }, - } - - assert.Equal(t, expected, records) -} - -func TestClient_GetRecords_error(t *testing.T) { - client := mockBuilder(). - Route("GET /services/test/zones/example.com./records", - servermock.ResponseFromFixture("errors.xml")). - Build(t) - - _, err := client.GetRecords(t.Context(), "test", "example.com.") - require.ErrorIs(t, err, Error{ - Text: "Access token expired or not found", - Code: "4097", - }) -} - -func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("PUT /services/test/zones/example.com./records", - servermock.ResponseFromFixture("records_PUT.xml"), - servermock.CheckHeader(). - WithContentType("text/xml")). - Build(t) - - rrs := []RR{ - { - Name: "@", - Type: "NS", - NS: &NS{Name: "ns4-l2.nic.ru."}, - }, - { - Name: "@", - Type: "NS", - NS: &NS{Name: "ns8-l2.nic.ru."}, - }, - } - - response, err := client.AddRecords(t.Context(), "test", "example.com.", rrs) - require.NoError(t, err) - - expected := []Zone{ - { - Admin: "123/NIC-REG", - HasChanges: "true", - ID: "228095", - IDNName: "test.ru", - Name: "test.ru", - Service: "testservice", - RR: []RR{ - { - ID: "210076", - Name: "@", - IDNName: "@", - Type: "NS", - NS: &NS{ - Name: "ns4-l2.nic.ru.", - IDNName: "ns4-l2.nic.ru.", - }, - }, - { - ID: "210077", - Name: "@", - IDNName: "@", - Type: "NS", - NS: &NS{ - Name: "ns8-l2.nic.ru.", - IDNName: "ns8-l2.nic.ru.", - }, - }, - }, - }, - } - - assert.Equal(t, expected, response) -} - -func TestClient_AddRecord_error(t *testing.T) { - client := mockBuilder(). - Route("PUT /services/test/zones/example.com./records", - servermock.ResponseFromFixture("errors.xml"), - servermock.CheckHeader(). - WithContentType("text/xml")). - Build(t) - - rrs := []RR{ - { - Name: "@", - Type: "NS", - NS: &NS{Name: "ns4-l2.nic.ru."}, - }, - { - Name: "@", - Type: "NS", - NS: &NS{Name: "ns8-l2.nic.ru."}, - }, - } - - _, err := client.AddRecords(t.Context(), "test", "example.com.", rrs) - require.ErrorIs(t, err, Error{ - Text: "Access token expired or not found", - Code: "4097", - }) -} - -func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /services/test/zones/example.com./records/123", - servermock.ResponseFromFixture("record_DELETE.xml")). - Build(t) - - err := client.DeleteRecord(t.Context(), "test", "example.com.", "123") - require.NoError(t, err) -} - -func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /services/test/zones/example.com./records/123", - servermock.ResponseFromFixture("errors.xml")). - Build(t) - - err := client.DeleteRecord(t.Context(), "test", "example.com.", "123") - require.ErrorIs(t, err, Error{ - Text: "Access token expired or not found", - Code: "4097", - }) -} - -func TestClient_CommitZone(t *testing.T) { - client := mockBuilder(). - Route("POST /services/test/zones/example.com./commit", - servermock.ResponseFromFixture("commit_POST.xml")). - Build(t) - - err := client.CommitZone(t.Context(), "test", "example.com.") - require.NoError(t, err) -} - -func TestClient_CommitZone_error(t *testing.T) { - client := mockBuilder(). - Route("POST /services/test/zones/example.com./commit", - servermock.ResponseFromFixture("errors.xml")). - Build(t) - - err := client.CommitZone(t.Context(), "test", "example.com.") - require.ErrorIs(t, err, Error{ - Text: "Access token expired or not found", - Code: "4097", - }) -} diff --git a/providers/dns/nicru/internal/fixtures/commit_POST.xml b/providers/dns/nicru/internal/fixtures/commit_POST.xml deleted file mode 100644 index 530a22d16..000000000 --- a/providers/dns/nicru/internal/fixtures/commit_POST.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - success - diff --git a/providers/dns/nicru/internal/fixtures/errors.xml b/providers/dns/nicru/internal/fixtures/errors.xml deleted file mode 100644 index 961b9a495..000000000 --- a/providers/dns/nicru/internal/fixtures/errors.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - fail - - Access token expired or not found - - diff --git a/providers/dns/nicru/internal/fixtures/record_DELETE.xml b/providers/dns/nicru/internal/fixtures/record_DELETE.xml deleted file mode 100644 index 530a22d16..000000000 --- a/providers/dns/nicru/internal/fixtures/record_DELETE.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - success - diff --git a/providers/dns/nicru/internal/fixtures/records_GET.xml b/providers/dns/nicru/internal/fixtures/records_GET.xml deleted file mode 100644 index a9df348f9..000000000 --- a/providers/dns/nicru/internal/fixtures/records_GET.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - success - - - - @ - @ - SOA - - - ns3-l2.nic.ru. - ns3-l2.nic.ru. - - - dns.nic.ru. - dns.nic.ru. - - 2011112002 - 1440 - 3600 - 2592000 - 600 - - - - @ - @ - NS - - ns3-l2.nic.ru. - ns3- l2.nic.ru. - - - - @ - @ - NS - - ns4-l2.nic.ru. - ns4-l2.nic.ru. - - - - @ - @ - NS - - ns8-l2.nic.ru. - ns8- l2.nic.ru. - - - - - diff --git a/providers/dns/nicru/internal/fixtures/records_PUT.xml b/providers/dns/nicru/internal/fixtures/records_PUT.xml deleted file mode 100644 index a3417a8f3..000000000 --- a/providers/dns/nicru/internal/fixtures/records_PUT.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - success - - - @@NSns4-l2.nic.ru.ns4-l2.nic.ru. - @@NSns8-l2.nic.ru.ns8-l2.nic.ru. - - - diff --git a/providers/dns/nicru/internal/fixtures/services_GET.xml b/providers/dns/nicru/internal/fixtures/services_GET.xml deleted file mode 100644 index 9534b0b34..000000000 --- a/providers/dns/nicru/internal/fixtures/services_GET.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - success - - - - - diff --git a/providers/dns/nicru/internal/fixtures/zones_GET.xml b/providers/dns/nicru/internal/fixtures/zones_GET.xml deleted file mode 100644 index efa2da9a2..000000000 --- a/providers/dns/nicru/internal/fixtures/zones_GET.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - success - - - - - - diff --git a/providers/dns/nicru/internal/fixtures/zones_all_GET.xml b/providers/dns/nicru/internal/fixtures/zones_all_GET.xml deleted file mode 100644 index efa2da9a2..000000000 --- a/providers/dns/nicru/internal/fixtures/zones_all_GET.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - success - - - - - - diff --git a/providers/dns/nicru/internal/identity.go b/providers/dns/nicru/internal/identity.go deleted file mode 100644 index b4281adbe..000000000 --- a/providers/dns/nicru/internal/identity.go +++ /dev/null @@ -1,64 +0,0 @@ -package internal - -import ( - "context" - "errors" - "fmt" - "net/http" - - "golang.org/x/oauth2" -) - -// OauthConfiguration credentials. -type OauthConfiguration struct { - OAuth2ClientID string - OAuth2SecretID string - Username string - Password string -} - -func (config *OauthConfiguration) Validate() error { - msg := " is missing in credentials information" - - if config.Username == "" { - return errors.New("username" + msg) - } - - if config.Password == "" { - return errors.New("password" + msg) - } - - if config.OAuth2ClientID == "" { - return errors.New("serviceID" + msg) - } - - if config.OAuth2SecretID == "" { - return errors.New("secret" + msg) - } - - return nil -} - -func NewOauthClient(ctx context.Context, config *OauthConfiguration) (*http.Client, error) { - err := config.Validate() - if err != nil { - return nil, err - } - - oauth2Config := oauth2.Config{ - ClientID: config.OAuth2ClientID, - ClientSecret: config.OAuth2SecretID, - Endpoint: oauth2.Endpoint{ - TokenURL: tokenURL, - AuthStyle: oauth2.AuthStyleInParams, - }, - Scopes: []string{".+:/dns-master/.+"}, - } - - oauth2Token, err := oauth2Config.PasswordCredentialsToken(ctx, config.Username, config.Password) - if err != nil { - return nil, fmt.Errorf("failed to create oauth2 token: %w", err) - } - - return oauth2Config.Client(ctx, oauth2Token), nil -} diff --git a/providers/dns/nicru/internal/types.go b/providers/dns/nicru/internal/types.go deleted file mode 100644 index ad3f8cc9a..000000000 --- a/providers/dns/nicru/internal/types.go +++ /dev/null @@ -1,214 +0,0 @@ -package internal - -import ( - "encoding/xml" - "fmt" -) - -type Request struct { - XMLName xml.Name `xml:"request"` - Text string `xml:",chardata"` - RRList *RRList `xml:"rr-list"` -} - -type RRList struct { - Text string `xml:",chardata"` - RR []RR `xml:"rr"` -} - -type RR struct { - Text string `xml:",chardata"` - ID string `xml:"id,attr,omitempty"` - Name string `xml:"name"` - IDNName string `xml:"idn-name"` - TTL string `xml:"ttl"` - Type string `xml:"type"` - SOA *SOA `xml:"soa,omitempty"` - A string `xml:"a,omitempty"` - AAAA string `xml:"aaaa,omitempty"` - CName *CName `xml:"cname,omitempty"` - NS *NS `xml:"ns,omitempty"` - MX *MX `xml:"mx,omitempty"` - SRV *SRV `xml:"srv,omitempty"` - PTR *PTR `xml:"ptr,omitempty"` - TXT *TXT `xml:"txt,omitempty"` - DName *DName `xml:"dname,omitempty"` - HInfo *HInfo `xml:"hinfo,omitempty"` - NAPTR *NAPTR `xml:"naptr,omitempty"` - RP *RP `xml:"rp,omitempty"` -} - -type SOA struct { - Text string `xml:",chardata"` - MName *MName `xml:"mname"` - RName *RName `xml:"rname"` - Serial string `xml:"serial"` - Refresh string `xml:"refresh"` - Retry string `xml:"retry"` - Expire string `xml:"expire"` - Minimum string `xml:"minimum"` -} - -type MName struct { - Text string `xml:",chardata"` - Name string `xml:"name"` - IDNName string `xml:"idn-name,omitempty"` -} - -type RName struct { - Text string `xml:",chardata"` - Name string `xml:"name"` - IDNName string `xml:"idn-name,omitempty"` -} - -type NS struct { - Text string `xml:",chardata"` - Name string `xml:"name"` - IDNName string `xml:"idn-name,omitempty"` -} - -type MX struct { - Text string `xml:",chardata"` - Preference string `xml:"preference"` - Exchange *Exchange `xml:"exchange"` -} - -type Exchange struct { - Name string `xml:"name"` -} - -type SRV struct { - Text string `xml:",chardata"` - Priority string `xml:"priority"` - Weight string `xml:"weight"` - Port string `xml:"port"` - Target *Target `xml:"target"` -} - -type Target struct { - Text string `xml:",chardata"` - Name string `xml:"name"` -} - -type PTR struct { - Text string `xml:",chardata"` - Name string `xml:"name"` -} - -type HInfo struct { - Text string `xml:",chardata"` - Hardware string `xml:"hardware"` - OS string `xml:"os"` -} - -type NAPTR struct { - Text string `xml:",chardata"` - Order string `xml:"order"` - Preference string `xml:"preference"` - Flags string `xml:"flags"` - Service string `xml:"service"` - Regexp string `xml:"regexp"` - Replacement *Replacement `xml:"replacement"` -} - -type Replacement struct { - Text string `xml:",chardata"` - Name string `xml:"name"` -} - -type RP struct { - Text string `xml:",chardata"` - MboxDName *MboxDName `xml:"mbox-dname"` - TxtDName *TxtDName `xml:"txt-dname"` -} - -type MboxDName struct { - Text string `xml:",chardata"` - Name string `xml:"name"` -} - -type TxtDName struct { - Text string `xml:",chardata"` - Name string `xml:"name"` -} - -type CName struct { - Text string `xml:",chardata"` - Name string `xml:"name"` - IDNName string `xml:"idn-name,omitempty"` -} - -type DName struct { - Text string `xml:",chardata"` - Name string `xml:"name"` -} - -type TXT struct { - Text string `xml:",chardata"` - String string `xml:"string"` -} - -type Response struct { - XMLName xml.Name `xml:"response"` - Text string `xml:",chardata"` - Status string `xml:"status"` - Data *Data `xml:"data"` - Errors Errors `xml:"errors"` -} - -type Data struct { - Text string `xml:",chardata"` - Service []Service `xml:"service"` - Zone []Zone `xml:"zone"` - Address []string `xml:"address"` - Revision []Revision `xml:"revision"` -} - -type Errors struct { - Text string `xml:",chardata"` - Error Error `xml:"error"` -} - -type Error struct { - Text string `xml:",chardata"` - Code string `xml:"code,attr"` -} - -func (e Error) Error() string { - return fmt.Sprintf("%s (code %s)", e.Text, e.Code) -} - -type Service struct { - Text string `xml:",chardata"` - Admin string `xml:"admin,attr"` - DomainsLimit string `xml:"domains-limit,attr"` - DomainsNum string `xml:"domains-num,attr"` - Enable string `xml:"enable,attr"` - HasPrimary string `xml:"has-primary,attr"` - Name string `xml:"name,attr"` - Payer string `xml:"payer,attr"` - Tariff string `xml:"tariff,attr"` - RRLimit string `xml:"rr-limit,attr"` - RRNum string `xml:"rr-num,attr"` -} - -type Zone struct { - Text string `xml:",chardata"` - Admin string `xml:"admin,attr"` - Enable string `xml:"enable,attr"` - HasChanges string `xml:"has-changes,attr"` - HasPrimary string `xml:"has-primary,attr"` - ID string `xml:"id,attr"` - IDNName string `xml:"idn-name,attr"` - Name string `xml:"name,attr"` - Payer string `xml:"payer,attr"` - Service string `xml:"service,attr"` - RR []RR `xml:"rr"` -} - -type Revision struct { - Text string `xml:",chardata"` - Date string `xml:"date,attr"` - IP string `xml:"ip,attr"` - Number string `xml:"number,attr"` -} diff --git a/providers/dns/nicru/nicru.go b/providers/dns/nicru/nicru.go deleted file mode 100644 index cf4255bdb..000000000 --- a/providers/dns/nicru/nicru.go +++ /dev/null @@ -1,239 +0,0 @@ -// Package nicru implements a DNS provider for solving the DNS-01 challenge using RU Center. -package nicru - -import ( - "context" - "errors" - "fmt" - "strconv" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/nicru/internal" -) - -// Environment variables names. -const ( - envNamespace = "NICRU_" - - EnvUsername = envNamespace + "USER" - EnvPassword = envNamespace + "PASSWORD" - EnvServiceID = envNamespace + "SERVICE_ID" - EnvSecret = envNamespace + "SECRET" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - TTL int - Username string - Password string - ServiceID string - Secret string - PropagationTimeout time.Duration - PollingInterval time.Duration -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, 30), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 1*time.Minute), - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - client *internal.Client - config *Config -} - -// NewDNSProvider returns a DNSProvider instance configured for RU Center. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvUsername, EnvPassword, EnvServiceID, EnvSecret) - if err != nil { - return nil, fmt.Errorf("nicru: %w", err) - } - - config := NewDefaultConfig() - config.Username = values[EnvUsername] - config.Password = values[EnvPassword] - config.ServiceID = values[EnvServiceID] - config.Secret = values[EnvSecret] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for RU Center. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("nicru: the configuration of the DNS provider is nil") - } - - clientCfg := &internal.OauthConfiguration{ - OAuth2ClientID: config.ServiceID, - OAuth2SecretID: config.Secret, - Username: config.Username, - Password: config.Password, - } - - oauthClient, err := internal.NewOauthClient(context.Background(), clientCfg) - if err != nil { - return nil, fmt.Errorf("nicru: %w", err) - } - - client, err := internal.NewClient(clientdebug.Wrap(oauthClient)) - if err != nil { - return nil, fmt.Errorf("nicru: unable to build API client: %w", err) - } - - return &DNSProvider{ - client: client, - config: config, - }, nil -} - -// Present creates a TXT record to fulfill the dns-01 challenge. -func (d *DNSProvider) Present(domain, _, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("nicru: could not find zone for domain %q: %w", domain, err) - } - - authZone = dns01.UnFqdn(authZone) - - zone, err := d.findZone(ctx, authZone) - if err != nil { - return fmt.Errorf("nicru: find zone: %w", err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("nicru: %w", err) - } - - records, err := d.client.GetRecords(ctx, zone.Service, authZone) - if err != nil { - return fmt.Errorf("nicru: get records: %w", err) - } - - for _, record := range records { - if record.TXT == nil { - continue - } - - if record.TXT.Text == subDomain && record.TXT.String == info.Value { - return nil - } - } - - rrs := []internal.RR{{ - Name: subDomain, - TTL: strconv.Itoa(d.config.TTL), - Type: "TXT", - TXT: &internal.TXT{String: info.Value}, - }} - - _, err = d.client.AddRecords(ctx, zone.Service, authZone, rrs) - if err != nil { - return fmt.Errorf("nicru: add records: %w", err) - } - - err = d.client.CommitZone(ctx, zone.Service, authZone) - if err != nil { - return fmt.Errorf("nicru: commit zone: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("nicru: could not find zone for domain %q: %w", domain, err) - } - - authZone = dns01.UnFqdn(authZone) - - zone, err := d.findZone(ctx, authZone) - if err != nil { - return fmt.Errorf("nicru: find zone: %w", err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("nicru: %w", err) - } - - records, err := d.client.GetRecords(ctx, zone.Service, authZone) - if err != nil { - return fmt.Errorf("nicru: get records: %w", err) - } - - subDomain = dns01.UnFqdn(subDomain) - - for _, record := range records { - if record.TXT == nil { - continue - } - - if record.Name != subDomain || record.TXT.String != info.Value { - continue - } - - err = d.client.DeleteRecord(ctx, zone.Service, authZone, record.ID) - if err != nil { - return fmt.Errorf("nicru: delete record: %w", err) - } - } - - err = d.client.CommitZone(ctx, zone.Service, authZone) - if err != nil { - return fmt.Errorf("nicru: commit zone: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -func (d *DNSProvider) findZone(ctx context.Context, authZone string) (*internal.Zone, error) { - zones, err := d.client.ListZones(ctx) - if err != nil { - return nil, fmt.Errorf("unable to fetch dns zones: %w", err) - } - - if len(zones) == 0 { - return nil, errors.New("no zones found") - } - - for _, zone := range zones { - if zone.Name == authZone { - return &zone, nil - } - } - - return nil, fmt.Errorf("zone not found for %s", authZone) -} diff --git a/providers/dns/nicru/nicru.toml b/providers/dns/nicru/nicru.toml deleted file mode 100644 index f955511a2..000000000 --- a/providers/dns/nicru/nicru.toml +++ /dev/null @@ -1,41 +0,0 @@ -Name = "RU CENTER" -Description = '''''' -URL = "https://nic.ru/" -Code = "nicru" -Since = "v4.24.0" - -Example = ''' -NICRU_USER="" \ -NICRU_PASSWORD="" \ -NICRU_SERVICE_ID="" \ -NICRU_SECRET="" \ -lego --dns nicru -d '*.example.com' -d example.com run -''' - -Additional = ''' -## Credential information - -You can find information about service ID and secret https://www.nic.ru/manager/oauth.cgi?step=oauth.app_list - -| ENV Variable | Parameter from page | Example | -|---------------------|--------------------------------|-------------------| -| NICRU_USER | Username (Number of agreement) | NNNNNNN/NIC-D | -| NICRU_PASSWORD | Password account | | -| NICRU_SERVICE_ID | Application ID | hex-based, len 32 | -| NICRU_SECRET | Identity endpoint | string len 91 | -''' - -[Configuration] - [Configuration.Credentials] - NICRU_USER = "Agreement for an account in RU CENTER" - NICRU_PASSWORD = "Password for an account in RU CENTER" - NICRU_SERVICE_ID = "Service ID for application in DNS-hosting RU CENTER" - NICRU_SECRET = "Secret for application in DNS-hosting RU CENTER" - NICRU_SERVICE_NAME = "Service Name for DNS-hosting RU CENTER" - [Configuration.Additional] - NICRU_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 60)" - NICRU_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 600)" - NICRU_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 30)" - -[Links] - API = "https://www.nic.ru/help/api-dns-hostinga_3643.html" diff --git a/providers/dns/nicru/nicru_test.go b/providers/dns/nicru/nicru_test.go deleted file mode 100644 index 7e71f9d2c..000000000 --- a/providers/dns/nicru/nicru_test.go +++ /dev/null @@ -1,195 +0,0 @@ -package nicru - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const ( - fakeServiceID = "2519234972459cdfa23423adf143324f" - fakeSecret = "oo5ahrie0aiPho3Vee4siupoPhahdahCh1thiesohru" - fakeUsername = "1234567/NIC-D" - fakePassword = "einge8Goo2eBaiXievuj" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvUsername, EnvPassword, EnvServiceID, EnvSecret).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvServiceID: fakeServiceID, - EnvSecret: fakeSecret, - EnvUsername: fakeUsername, - EnvPassword: fakePassword, - }, - expected: "nicru: failed to create oauth2 token: oauth2: \"unauthorized_client\"", - }, - { - desc: "missing serviceID", - envVars: map[string]string{ - EnvSecret: fakeSecret, - EnvUsername: fakeUsername, - EnvPassword: fakePassword, - }, - expected: "nicru: some credentials information are missing: NICRU_SERVICE_ID", - }, - { - desc: "missing secret", - envVars: map[string]string{ - EnvServiceID: fakeServiceID, - EnvUsername: fakeUsername, - EnvPassword: fakePassword, - }, - expected: "nicru: some credentials information are missing: NICRU_SECRET", - }, - { - desc: "missing username", - envVars: map[string]string{ - EnvServiceID: fakeServiceID, - EnvSecret: fakeSecret, - EnvPassword: fakePassword, - }, - expected: "nicru: some credentials information are missing: NICRU_USER", - }, - { - desc: "missing password", - envVars: map[string]string{ - EnvServiceID: fakeServiceID, - EnvSecret: fakeSecret, - EnvUsername: fakeUsername, - }, - expected: "nicru: some credentials information are missing: NICRU_PASSWORD", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - config *Config - expected string - }{ - { - desc: "success", - config: &Config{ - ServiceID: fakeServiceID, - Secret: fakeSecret, - Username: fakeUsername, - Password: fakePassword, - }, - expected: "nicru: failed to create oauth2 token: oauth2: \"unauthorized_client\"", - }, - { - desc: "nil config", - config: nil, - expected: "nicru: the configuration of the DNS provider is nil", - }, - { - desc: "missing username", - config: &Config{ - ServiceID: fakeServiceID, - Password: fakePassword, - }, - expected: "nicru: username is missing in credentials information", - }, - { - desc: "missing password", - config: &Config{ - ServiceID: fakeServiceID, - Secret: fakeSecret, - Username: fakeUsername, - }, - expected: "nicru: password is missing in credentials information", - }, - { - desc: "missing secret", - config: &Config{ - ServiceID: fakeServiceID, - Username: fakeUsername, - Password: fakePassword, - }, - expected: "nicru: secret is missing in credentials information", - }, - { - desc: "missing serviceID", - config: &Config{ - Secret: fakeSecret, - Username: fakeUsername, - Password: fakePassword, - }, - expected: "nicru: serviceID is missing in credentials information", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - p, err := NewDNSProviderConfig(test.config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/nifcloud/internal/client.go b/providers/dns/nifcloud/internal/client.go index 0f3851883..02d11fead 100644 --- a/providers/dns/nifcloud/internal/client.go +++ b/providers/dns/nifcloud/internal/client.go @@ -59,7 +59,6 @@ func (c *Client) ChangeResourceRecordSets(ctx context.Context, hostedZoneID stri } output := &ChangeResourceRecordSetsResponse{} - err = c.do(req, output) if err != nil { return nil, err @@ -78,7 +77,6 @@ func (c *Client) GetChange(ctx context.Context, statusID string) (*GetChangeResp } output := &GetChangeResponse{} - err = c.do(req, output) if err != nil { return nil, err @@ -123,7 +121,12 @@ func (c *Client) do(req *http.Request, result any) error { func (c *Client) sign(req *http.Request) error { if req.Header.Get("Date") == "" { - req.Header.Set("Date", time.Now().UTC().Format(http.TimeFormat)) + location, err := time.LoadLocation("GMT") + if err != nil { + return err + } + + req.Header.Set("Date", time.Now().In(location).Format(time.RFC1123)) } if req.URL.Path == "" { @@ -131,7 +134,6 @@ func (c *Client) sign(req *http.Request) error { } mac := hmac.New(sha1.New, []byte(c.secretKey)) - _, err := mac.Write([]byte(req.Header.Get("Date"))) if err != nil { return err @@ -151,7 +153,6 @@ func newXMLRequest(ctx context.Context, method string, endpoint *url.URL, payloa if payload != nil { body.WriteString(xml.Header) - err := xml.NewEncoder(body).Encode(payload) if err != nil { return nil, fmt.Errorf("failed to create request XML body: %w", err) @@ -174,7 +175,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) errResp := &ErrorResponse{} - err := xml.Unmarshal(raw, errResp) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/nifcloud/internal/client_test.go b/providers/dns/nifcloud/internal/client_test.go index 501265ada..06c4921e0 100644 --- a/providers/dns/nifcloud/internal/client_test.go +++ b/providers/dns/nifcloud/internal/client_test.go @@ -1,35 +1,38 @@ package internal import ( + "context" + "fmt" "net/http" "net/http/httptest" "net/url" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("A", "B") - if err != nil { - return nil, err - } +func setupTest(t *testing.T, responseBody string, statusCode int) *Client { + t.Helper() - client.HTTPClient = server.Client() - client.BaseURL, _ = url.Parse(server.URL) + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(statusCode) + _, _ = fmt.Fprintln(w, responseBody) + }) - return client, nil - }, - servermock.CheckHeader(). - WithRegexp("X-Nifty-Authorization", "NIFTY3-HTTPS NiftyAccessKeyId=A,Algorithm=HmacSHA1,Signature=.+"), - ) + server := httptest.NewServer(handler) + t.Cleanup(server.Close) + + client, err := NewClient("A", "B") + require.NoError(t, err) + + client.HTTPClient = server.Client() + client.BaseURL, _ = url.Parse(server.URL) + + return client } -func TestClient_ChangeResourceRecordSets(t *testing.T) { +func TestChangeResourceRecordSets(t *testing.T) { responseBody := ` @@ -40,12 +43,9 @@ func TestClient_ChangeResourceRecordSets(t *testing.T) { ` - client := mockBuilder(). - Route("POST /", servermock.RawStringResponse(responseBody), - servermock.CheckHeader().WithContentType("text/xml; charset=utf-8")). - Build(t) + client := setupTest(t, responseBody, http.StatusOK) - res, err := client.ChangeResourceRecordSets(t.Context(), "example.com", ChangeResourceRecordSetsRequest{}) + res, err := client.ChangeResourceRecordSets(context.Background(), "example.com", ChangeResourceRecordSetsRequest{}) require.NoError(t, err) assert.Equal(t, "xxxxx", res.ChangeInfo.ID) @@ -53,7 +53,7 @@ func TestClient_ChangeResourceRecordSets(t *testing.T) { assert.Equal(t, "2015-08-05T00:00:00.000Z", res.ChangeInfo.SubmittedAt) } -func TestClient_ChangeResourceRecordSets_errors(t *testing.T) { +func TestChangeResourceRecordSetsErrors(t *testing.T) { testCases := []struct { desc string responseBody string @@ -90,22 +90,16 @@ func TestClient_ChangeResourceRecordSets_errors(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - client := mockBuilder(). - Route("POST /", - servermock.RawStringResponse(test.responseBody). - WithStatusCode(test.statusCode), - servermock.CheckHeader(). - WithContentType("text/xml; charset=utf-8")). - Build(t) + client := setupTest(t, test.responseBody, test.statusCode) - res, err := client.ChangeResourceRecordSets(t.Context(), "example.com", ChangeResourceRecordSetsRequest{}) + res, err := client.ChangeResourceRecordSets(context.Background(), "example.com", ChangeResourceRecordSetsRequest{}) assert.Nil(t, res) assert.EqualError(t, err, test.expected) }) } } -func TestClient_GetChange(t *testing.T) { +func TestGetChange(t *testing.T) { responseBody := ` @@ -116,11 +110,9 @@ func TestClient_GetChange(t *testing.T) { ` - client := mockBuilder(). - Route("GET /", servermock.RawStringResponse(responseBody)). - Build(t) + client := setupTest(t, responseBody, http.StatusOK) - res, err := client.GetChange(t.Context(), "12345") + res, err := client.GetChange(context.Background(), "12345") require.NoError(t, err) assert.Equal(t, "xxxxx", res.ChangeInfo.ID) @@ -128,7 +120,7 @@ func TestClient_GetChange(t *testing.T) { assert.Equal(t, "2015-08-05T00:00:00.000Z", res.ChangeInfo.SubmittedAt) } -func TestClient_GetChange_errors(t *testing.T) { +func TestGetChangeErrors(t *testing.T) { testCases := []struct { desc string responseBody string @@ -165,12 +157,9 @@ func TestClient_GetChange_errors(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - client := mockBuilder(). - Route("GET /", - servermock.RawStringResponse(test.responseBody).WithStatusCode(test.statusCode)). - Build(t) + client := setupTest(t, test.responseBody, test.statusCode) - res, err := client.GetChange(t.Context(), "12345") + res, err := client.GetChange(context.Background(), "12345") assert.Nil(t, res) assert.EqualError(t, err, test.expected) }) diff --git a/providers/dns/nifcloud/nifcloud.go b/providers/dns/nifcloud/nifcloud.go index ced7eff09..b059e562e 100644 --- a/providers/dns/nifcloud/nifcloud.go +++ b/providers/dns/nifcloud/nifcloud.go @@ -9,12 +9,9 @@ import ( "net/url" "time" - "github.com/cenkalti/backoff/v5" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/platform/wait" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/nifcloud/internal" ) @@ -32,8 +29,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string @@ -95,8 +90,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - if config.BaseURL != "" { baseURL, err := url.Parse(config.BaseURL) if err != nil { @@ -111,29 +104,23 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) - err := d.changeRecord(ctx, "CREATE", info.EffectiveFQDN, info.Value, d.config.TTL) + err := d.changeRecord("CREATE", info.EffectiveFQDN, info.Value, d.config.TTL) if err != nil { return fmt.Errorf("nifcloud: %w", err) } - return err } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) - err := d.changeRecord(ctx, "DELETE", info.EffectiveFQDN, info.Value, d.config.TTL) + err := d.changeRecord("DELETE", info.EffectiveFQDN, info.Value, d.config.TTL) if err != nil { return fmt.Errorf("nifcloud: %w", err) } - return err } @@ -143,7 +130,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } -func (d *DNSProvider) changeRecord(ctx context.Context, action, fqdn, value string, ttl int) error { +func (d *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error { authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("could not find zone: %w", err) @@ -180,6 +167,8 @@ func (d *DNSProvider) changeRecord(ctx context.Context, action, fqdn, value stri }, } + ctx := context.Background() + resp, err := d.client.ChangeResourceRecordSets(ctx, dns01.UnFqdn(authZone), reqParams) if err != nil { return fmt.Errorf("failed to change record set: %w", err) @@ -187,20 +176,11 @@ func (d *DNSProvider) changeRecord(ctx context.Context, action, fqdn, value stri statusID := resp.ChangeInfo.ID - return wait.Retry(ctx, - func() error { - resp, err := d.client.GetChange(ctx, statusID) - if err != nil { - return fmt.Errorf("get change: %w", err) - } - - if resp.ChangeInfo.Status != "INSYNC" { - return fmt.Errorf("change status: %s", resp.ChangeInfo.Status) - } - - return nil - }, - backoff.WithBackOff(backoff.NewConstantBackOff(4*time.Second)), - backoff.WithMaxElapsedTime(120*time.Second), - ) + return wait.For("nifcloud", 120*time.Second, 4*time.Second, func() (bool, error) { + resp, err := d.client.GetChange(ctx, statusID) + if err != nil { + return false, fmt.Errorf("failed to query change status: %w", err) + } + return resp.ChangeInfo.Status == "INSYNC", nil + }) } diff --git a/providers/dns/nifcloud/nifcloud.toml b/providers/dns/nifcloud/nifcloud.toml index 3c43b1dc0..35d302aa8 100644 --- a/providers/dns/nifcloud/nifcloud.toml +++ b/providers/dns/nifcloud/nifcloud.toml @@ -7,7 +7,7 @@ Since = "v1.1.0" Example = ''' NIFCLOUD_ACCESS_KEY_ID=xxxx \ NIFCLOUD_SECRET_ACCESS_KEY=yyyy \ -lego --dns nifcloud -d '*.example.com' -d example.com run +lego --email you@example.com --dns nifcloud --domains my.example.org run ''' [Configuration] @@ -15,10 +15,10 @@ lego --dns nifcloud -d '*.example.com' -d example.com run NIFCLOUD_ACCESS_KEY_ID = "Access key" NIFCLOUD_SECRET_ACCESS_KEY = "Secret access key" [Configuration.Additional] - NIFCLOUD_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - NIFCLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - NIFCLOUD_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - NIFCLOUD_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + NIFCLOUD_POLLING_INTERVAL = "Time between DNS propagation check" + NIFCLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + NIFCLOUD_TTL = "The TTL of the TXT record used for the DNS challenge" + NIFCLOUD_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://mbaas.nifcloud.com/doc/current/rest/common/format.html" diff --git a/providers/dns/nifcloud/nifcloud_test.go b/providers/dns/nifcloud/nifcloud_test.go index 0eff98a71..9b635edfc 100644 --- a/providers/dns/nifcloud/nifcloud_test.go +++ b/providers/dns/nifcloud/nifcloud_test.go @@ -57,7 +57,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -130,7 +129,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -144,7 +142,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/njalla/internal/client.go b/providers/dns/njalla/internal/client.go index d2893253f..f7e0023ae 100644 --- a/providers/dns/njalla/internal/client.go +++ b/providers/dns/njalla/internal/client.go @@ -46,7 +46,6 @@ func (c *Client) AddRecord(ctx context.Context, record Record) (*Record, error) } var result APIResponse[*Record] - err = c.do(req, &result) if err != nil { return nil, err @@ -56,7 +55,7 @@ func (c *Client) AddRecord(ctx context.Context, record Record) (*Record, error) } // RemoveRecord removes a record. -func (c *Client) RemoveRecord(ctx context.Context, id, domain string) error { +func (c *Client) RemoveRecord(ctx context.Context, id string, domain string) error { data := APIRequest{ Method: "remove-record", Params: Record{ @@ -93,7 +92,6 @@ func (c *Client) ListRecords(ctx context.Context, domain string) ([]Record, erro } var result APIResponse[Records] - err = c.do(req, &result) if err != nil { return nil, err @@ -129,7 +127,7 @@ func (c *Client) do(req *http.Request, result Response) error { return result.GetError() } -func newJSONRequest(ctx context.Context, method, endpoint string, payload any) (*http.Request, error) { +func newJSONRequest(ctx context.Context, method string, endpoint string, payload any) (*http.Request, error) { buf := new(bytes.Buffer) if payload != nil { diff --git a/providers/dns/njalla/internal/client_test.go b/providers/dns/njalla/internal/client_test.go index a7e60aefd..3f173db62 100644 --- a/providers/dns/njalla/internal/client_test.go +++ b/providers/dns/njalla/internal/client_test.go @@ -1,31 +1,76 @@ package internal import ( + "context" + "encoding/json" + "fmt" + "net/http" "net/http/httptest" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func setupClient(server *httptest.Server) (*Client, error) { +func setupTest(t *testing.T, handler func(http.ResponseWriter, *http.Request)) *Client { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + token := req.Header.Get(authorizationHeader) + if token != "Njalla secret" { + _, _ = rw.Write([]byte(`{"jsonrpc":"2.0", "Error": {"code": 403, "message": "Invalid token."}}`)) + return + } + + if handler != nil { + handler(rw, req) + } else { + _, _ = rw.Write([]byte(`{"jsonrpc":"2.0"}`)) + } + }) + client := NewClient("secret") client.apiEndpoint = server.URL - client.HTTPClient = server.Client() - return client, nil + return client } func TestClient_AddRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("Njalla secret"), - ). - Route("POST /", - servermock.ResponseFromFixture("add_record.json"), - servermock.CheckRequestJSONBodyFromFixture("add_record-request.json")). - Build(t) + client := setupTest(t, func(rw http.ResponseWriter, req *http.Request) { + apiReq := struct { + Method string `json:"method"` + Params Record `json:"params"` + }{} + + err := json.NewDecoder(req.Body).Decode(&apiReq) + if err != nil { + http.Error(rw, "failed to marshal test request body", http.StatusInternalServerError) + return + } + + apiReq.Params.ID = "123" + + resp := map[string]interface{}{ + "jsonrpc": "2.0", + "id": "897", + "result": apiReq.Params, + } + + err = json.NewEncoder(rw).Encode(resp) + if err != nil { + http.Error(rw, "failed to marshal test response", http.StatusInternalServerError) + return + } + }) record := Record{ Content: "foobar", @@ -35,7 +80,7 @@ func TestClient_AddRecord(t *testing.T) { Type: "TXT", } - result, err := client.AddRecord(t.Context(), record) + result, err := client.AddRecord(context.Background(), record) require.NoError(t, err) expected := &Record{ @@ -50,13 +95,7 @@ func TestClient_AddRecord(t *testing.T) { } func TestClient_AddRecord_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("Njalla invalid"), - ). - Route("POST /", servermock.ResponseFromFixture("auth_error.json")). - Build(t) - + client := setupTest(t, nil) client.token = "invalid" record := Record{ @@ -67,23 +106,58 @@ func TestClient_AddRecord_error(t *testing.T) { Type: "TXT", } - result, err := client.AddRecord(t.Context(), record) - require.EqualError(t, err, "code: 403, message: Invalid token.") + result, err := client.AddRecord(context.Background(), record) + require.Error(t, err) assert.Nil(t, result) } func TestClient_ListRecords(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("Njalla secret"), - ). - Route("POST /", - servermock.ResponseFromFixture("list_records.json"), - servermock.CheckRequestJSONBodyFromFixture("list_records-request.json")). - Build(t) + client := setupTest(t, func(rw http.ResponseWriter, req *http.Request) { + apiReq := struct { + Method string `json:"method"` + Params Record `json:"params"` + }{} - records, err := client.ListRecords(t.Context(), "example.com") + err := json.NewDecoder(req.Body).Decode(&apiReq) + if err != nil { + http.Error(rw, "failed to marshal test request body", http.StatusInternalServerError) + return + } + + resp := map[string]interface{}{ + "jsonrpc": "2.0", + "id": "897", + "result": Records{ + Records: []Record{ + { + ID: "1", + Domain: apiReq.Params.Domain, + Content: "test", + Name: "test01", + TTL: 300, + Type: "TXT", + }, + { + ID: "2", + Domain: apiReq.Params.Domain, + Content: "txtTxt", + Name: "test02", + TTL: 120, + Type: "TXT", + }, + }, + }, + } + + err = json.NewEncoder(rw).Encode(resp) + if err != nil { + http.Error(rw, "failed to marshal test response", http.StatusInternalServerError) + return + } + }) + + records, err := client.ListRecords(context.Background(), "example.com") require.NoError(t, err) expected := []Record{ @@ -109,43 +183,49 @@ func TestClient_ListRecords(t *testing.T) { } func TestClient_ListRecords_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("Njalla invalid"), - ). - Route("POST /", servermock.ResponseFromFixture("auth_error.json")). - Build(t) - + client := setupTest(t, nil) client.token = "invalid" - records, err := client.ListRecords(t.Context(), "example.com") - require.EqualError(t, err, "code: 403, message: Invalid token.") + records, err := client.ListRecords(context.Background(), "example.com") + require.Error(t, err) assert.Empty(t, records) } func TestClient_RemoveRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("Njalla secret"), - ). - Route("POST /", - servermock.RawStringResponse(`{"jsonrpc":"2.0"}`), - servermock.CheckRequestJSONBodyFromFixture("remove_record-request.json")). - Build(t) + client := setupTest(t, func(rw http.ResponseWriter, req *http.Request) { + apiReq := struct { + Method string `json:"method"` + Params Record `json:"params"` + }{} - err := client.RemoveRecord(t.Context(), "123", "example.com") + err := json.NewDecoder(req.Body).Decode(&apiReq) + if err != nil { + http.Error(rw, "failed to marshal test request body", http.StatusInternalServerError) + return + } + + if apiReq.Params.ID == "" { + _, _ = rw.Write([]byte(`{"jsonrpc":"2.0", "Error": {"code": 400, "message": ""missing ID"}}`)) + return + } + + if apiReq.Params.Domain == "" { + _, _ = rw.Write([]byte(`{"jsonrpc":"2.0", "Error": {"code": 400, "message": ""missing domain"}}`)) + return + } + + _, _ = rw.Write([]byte(`{"jsonrpc":"2.0"}`)) + }) + + err := client.RemoveRecord(context.Background(), "123", "example.com") require.NoError(t, err) } func TestClient_RemoveRecord_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("Njalla secret"), - ). - Route("POST /", servermock.ResponseFromFixture("remove_record_error_missing_domain.json")). - Build(t) + client := setupTest(t, nil) + client.token = "invalid" - err := client.RemoveRecord(t.Context(), "123", "example.com") - require.EqualError(t, err, "code: 400, message: missing domain") + err := client.RemoveRecord(context.Background(), "123", "example.com") + require.Error(t, err) } diff --git a/providers/dns/njalla/internal/fixtures/add_record-request.json b/providers/dns/njalla/internal/fixtures/add_record-request.json deleted file mode 100644 index a85e1aaf1..000000000 --- a/providers/dns/njalla/internal/fixtures/add_record-request.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "method": "add-record", - "params": { - "content": "foobar", - "domain": "test", - "name": "example.com", - "ttl": 300, - "type": "TXT" - } -} diff --git a/providers/dns/njalla/internal/fixtures/add_record.json b/providers/dns/njalla/internal/fixtures/add_record.json deleted file mode 100644 index a537762bf..000000000 --- a/providers/dns/njalla/internal/fixtures/add_record.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "id": "897", - "jsonrpc": "2.0", - "result": { - "id": "123", - "content": "foobar", - "domain": "test", - "name": "example.com", - "ttl": 300, - "type": "TXT" - } -} diff --git a/providers/dns/njalla/internal/fixtures/auth_error.json b/providers/dns/njalla/internal/fixtures/auth_error.json deleted file mode 100644 index e9d07be51..000000000 --- a/providers/dns/njalla/internal/fixtures/auth_error.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "jsonrpc": "2.0", - "Error": { - "code": 403, - "message": "Invalid token." - } -} diff --git a/providers/dns/njalla/internal/fixtures/list_records-request.json b/providers/dns/njalla/internal/fixtures/list_records-request.json deleted file mode 100644 index ebe5ccf72..000000000 --- a/providers/dns/njalla/internal/fixtures/list_records-request.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "method": "list-records", - "params": { - "domain": "example.com" - } -} diff --git a/providers/dns/njalla/internal/fixtures/list_records.json b/providers/dns/njalla/internal/fixtures/list_records.json deleted file mode 100644 index a280a4b3f..000000000 --- a/providers/dns/njalla/internal/fixtures/list_records.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "id": "897", - "jsonrpc": "2.0", - "result": { - "records": [ - { - "id": "1", - "content": "test", - "domain": "example.com", - "name": "test01", - "ttl": 300, - "type": "TXT" - }, - { - "id": "2", - "content": "txtTxt", - "domain": "example.com", - "name": "test02", - "ttl": 120, - "type": "TXT" - } - ] - } -} diff --git a/providers/dns/njalla/internal/fixtures/remove_record-request.json b/providers/dns/njalla/internal/fixtures/remove_record-request.json deleted file mode 100644 index c96e94423..000000000 --- a/providers/dns/njalla/internal/fixtures/remove_record-request.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "method": "remove-record", - "params": { - "id": "123", - "domain": "example.com" - } -} diff --git a/providers/dns/njalla/internal/fixtures/remove_record_error_missing_domain.json b/providers/dns/njalla/internal/fixtures/remove_record_error_missing_domain.json deleted file mode 100644 index f65d254d0..000000000 --- a/providers/dns/njalla/internal/fixtures/remove_record_error_missing_domain.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "jsonrpc": "2.0", - "Error": { - "code": 400, - "message": "missing domain" - } -} diff --git a/providers/dns/njalla/internal/fixtures/remove_record_error_missing_id.json b/providers/dns/njalla/internal/fixtures/remove_record_error_missing_id.json deleted file mode 100644 index 544cd4d1c..000000000 --- a/providers/dns/njalla/internal/fixtures/remove_record_error_missing_id.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "jsonrpc": "2.0", - "Error": { - "code": 400, - "message": "missing ID" - } -} diff --git a/providers/dns/njalla/njalla.go b/providers/dns/njalla/njalla.go index 2f9aef8ea..fe23e8d6d 100644 --- a/providers/dns/njalla/njalla.go +++ b/providers/dns/njalla/njalla.go @@ -9,10 +9,8 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/njalla/internal" "github.com/miekg/dns" ) @@ -29,8 +27,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Token string @@ -91,8 +87,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, client: client, @@ -148,7 +142,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() - if !ok { return fmt.Errorf("njalla: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) } diff --git a/providers/dns/njalla/njalla.toml b/providers/dns/njalla/njalla.toml index ff4750b7d..e9670b837 100644 --- a/providers/dns/njalla/njalla.toml +++ b/providers/dns/njalla/njalla.toml @@ -6,17 +6,17 @@ Since = "v4.3.0" Example = ''' NJALLA_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --dns njalla -d '*.example.com' -d example.com run +lego --email you@example.com --dns njalla --domains my.example.org run ''' [Configuration] [Configuration.Credentials] NJALLA_TOKEN = "API token" [Configuration.Additional] - NJALLA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - NJALLA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - NJALLA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - NJALLA_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + NJALLA_POLLING_INTERVAL = "Time between DNS propagation check" + NJALLA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + NJALLA_TTL = "The TTL of the TXT record used for the DNS challenge" + NJALLA_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://njal.la/api/" diff --git a/providers/dns/njalla/njalla_test.go b/providers/dns/njalla/njalla_test.go index 61f106d75..f1489257b 100644 --- a/providers/dns/njalla/njalla_test.go +++ b/providers/dns/njalla/njalla_test.go @@ -36,7 +36,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -96,7 +95,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -110,7 +108,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/nodion/nodion.go b/providers/dns/nodion/nodion.go index 4bc887568..e1ce72e81 100644 --- a/providers/dns/nodion/nodion.go +++ b/providers/dns/nodion/nodion.go @@ -9,10 +9,8 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/nrdcg/nodion" ) @@ -28,8 +26,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIToken string @@ -94,8 +90,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, client: client, @@ -172,7 +166,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { d.zoneIDsMu.Lock() zoneID, ok := d.zoneIDs[token] d.zoneIDsMu.Unlock() - if !ok { return fmt.Errorf("nodion: unknown zone ID for '%s' '%s'", info.EffectiveFQDN, token) } @@ -208,9 +201,5 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("regru: failed to remove TXT records [domain: %s]: %w", dns01.UnFqdn(authZone), err) } - d.zoneIDsMu.Lock() - delete(d.zoneIDs, token) - d.zoneIDsMu.Unlock() - return nil } diff --git a/providers/dns/nodion/nodion.toml b/providers/dns/nodion/nodion.toml index c9db46e61..ae76b5f5b 100644 --- a/providers/dns/nodion/nodion.toml +++ b/providers/dns/nodion/nodion.toml @@ -6,17 +6,17 @@ Since = "v4.11.0" Example = ''' NODION_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns nodion -d '*.example.com' -d example.com run +lego --email myemail@example.com --dns nodion --domains my.example.org run ''' [Configuration] [Configuration.Credentials] NODION_API_TOKEN = "The API token" [Configuration.Additional] - NODION_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - NODION_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - NODION_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - NODION_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + NODION_POLLING_INTERVAL = "Time between DNS propagation check" + NODION_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + NODION_TTL = "The TTL of the TXT record used for the DNS challenge" + NODION_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.nodion.com/en/docs/dns/api/" diff --git a/providers/dns/nodion/nodion_test.go b/providers/dns/nodion/nodion_test.go index 0ec5c1627..fbf4b89eb 100644 --- a/providers/dns/nodion/nodion_test.go +++ b/providers/dns/nodion/nodion_test.go @@ -34,7 +34,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -92,7 +91,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -106,7 +104,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/ns1/ns1.go b/providers/dns/ns1/ns1.go index 6a7846e85..ffa4b1b70 100644 --- a/providers/dns/ns1/ns1.go +++ b/providers/dns/ns1/ns1.go @@ -7,11 +7,9 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "gopkg.in/ns1/ns1-go.v2/rest" "gopkg.in/ns1/ns1-go.v2/rest/model/dns" ) @@ -28,8 +26,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string @@ -81,12 +77,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("ns1: credentials missing") } - if config.HTTPClient == nil { - // Because the rest.NewClient uses the http.DefaultClient. - config.HTTPClient = &http.Client{Timeout: 10 * time.Second} - } - - client := rest.NewClient(clientdebug.Wrap(config.HTTPClient), rest.SetAPIKey(config.APIKey)) + client := rest.NewClient(config.HTTPClient, rest.SetAPIKey(config.APIKey)) return &DNSProvider{client: client, config: config}, nil } @@ -147,12 +138,10 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } name := dns01.UnFqdn(info.EffectiveFQDN) - _, err = d.client.Records.Delete(zone.Zone, name, "TXT") if err != nil { return fmt.Errorf("ns1: failed to delete record [zone: %q, domain: %q]: %w", zone.Zone, name, err) } - return nil } diff --git a/providers/dns/ns1/ns1.toml b/providers/dns/ns1/ns1.toml index 829663bf5..e65bacdfa 100644 --- a/providers/dns/ns1/ns1.toml +++ b/providers/dns/ns1/ns1.toml @@ -6,17 +6,17 @@ Since = "v0.4.0" Example = ''' NS1_API_KEY=xxxx \ -lego --dns ns1 -d '*.example.com' -d example.com run +lego --email you@example.com --dns ns1 --domains my.example.org run ''' [Configuration] [Configuration.Credentials] NS1_API_KEY = "API key" [Configuration.Additional] - NS1_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - NS1_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - NS1_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - NS1_HTTP_TIMEOUT = "API request timeout in seconds (Default: 10)" + NS1_POLLING_INTERVAL = "Time between DNS propagation check" + NS1_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + NS1_TTL = "The TTL of the TXT record used for the DNS challenge" + NS1_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://ns1.com/api" diff --git a/providers/dns/ns1/ns1_test.go b/providers/dns/ns1/ns1_test.go index 82fa70c52..6df6b4afb 100644 --- a/providers/dns/ns1/ns1_test.go +++ b/providers/dns/ns1/ns1_test.go @@ -37,7 +37,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -97,7 +96,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -111,7 +109,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/octenium/fixtures/add_dns_record.json b/providers/dns/octenium/fixtures/add_dns_record.json deleted file mode 100644 index 25edcdf11..000000000 --- a/providers/dns/octenium/fixtures/add_dns_record.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "api-status": "success", - "api-response": { - "record": { - "type": "TXT", - "name": "_acme-challenge.example.com.", - "ttl": 120, - "value": "w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI", - "raw": { - "txtdata": "w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI" - } - } - } -} diff --git a/providers/dns/octenium/fixtures/delete_dns_record.json b/providers/dns/octenium/fixtures/delete_dns_record.json deleted file mode 100644 index 2aa9415cc..000000000 --- a/providers/dns/octenium/fixtures/delete_dns_record.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "api-status": "success", - "api-response": { - "deleted": { - "count": 1, - "lines": [ - 123 - ] - } - } -} diff --git a/providers/dns/octenium/fixtures/list_dns_records.json b/providers/dns/octenium/fixtures/list_dns_records.json deleted file mode 100644 index 405afff11..000000000 --- a/providers/dns/octenium/fixtures/list_dns_records.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "api-status": "success", - "api-response": { - "records": [ - { - "line": 31, - "type": "TXT", - "name": "_dmarc.example.com.", - "ttl": 300, - "value": "xxx", - "raw": { - "txtdata": "xxx" - } - }, - { - "line": 123, - "type": "TXT", - "name": "_acme-challenge.example.com.", - "ttl": 300, - "value": "w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI", - "raw": { - "txtdata": "w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI" - } - } - ] - } -} diff --git a/providers/dns/octenium/fixtures/list_domains.json b/providers/dns/octenium/fixtures/list_domains.json deleted file mode 100644 index a62febcda..000000000 --- a/providers/dns/octenium/fixtures/list_domains.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "api-status": "success", - "api-response": { - "domains": { - "2976": { - "domain-name": "example.com", - "registration-date": "21\/08\/2025", - "expiration-date": "-", - "status": "active" - } - } - } -} diff --git a/providers/dns/octenium/internal/client.go b/providers/dns/octenium/internal/client.go deleted file mode 100644 index 474770aeb..000000000 --- a/providers/dns/octenium/internal/client.go +++ /dev/null @@ -1,204 +0,0 @@ -package internal - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "strings" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - querystring "github.com/google/go-querystring/query" -) - -const defaultBaseURL = "https://api.panel.octenium.com/" - -const statusSuccess = "success" - -// Client the Octenium API client. -type Client struct { - apiKey string - - BaseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(apiKey string) (*Client, error) { - if apiKey == "" { - return nil, errors.New("credentials missing") - } - - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - apiKey: apiKey, - BaseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -// ListDomains retrieves a list of domains. -// https://octenium.com/api#tag/Domains/operation/listdomains -func (c *Client) ListDomains(ctx context.Context, domain string) (map[string]Domain, error) { - endpoint := c.BaseURL.JoinPath("domains") - - data := endpoint.Query() - data.Set("domain-name", domain) - endpoint.RawQuery = data.Encode() - - req, err := newRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - result := &DomainsResponse{} - - err = c.do(req, result) - if err != nil { - return nil, err - } - - return result.Domains, nil -} - -// ListDNSRecords retrieves a list of DNS records. -// https://octenium.com/api#tag/Domains-DNS/operation/domains-dns-records-list -func (c *Client) ListDNSRecords(ctx context.Context, orderID, recordType string) ([]Record, error) { - endpoint := c.BaseURL.JoinPath("domains", "dns-records", "list") - - data := make(url.Values) - data.Set("order-id", orderID) - data.Set("types[]", recordType) - - req, err := newRequest(ctx, http.MethodPost, endpoint, data) - if err != nil { - return nil, err - } - - result := &ListRecordsResponse{} - - err = c.do(req, result) - if err != nil { - return nil, err - } - - return result.Records, nil -} - -// AddDNSRecord adds a DNS record. -// https://octenium.com/api#tag/Domains-DNS/operation/domains-dns-records-add -func (c *Client) AddDNSRecord(ctx context.Context, orderID string, record Record) (*Record, error) { - endpoint := c.BaseURL.JoinPath("domains", "dns-records", "add") - - data, err := querystring.Values(record) - if err != nil { - return nil, err - } - - data.Set("order-id", orderID) - - req, err := newRequest(ctx, http.MethodPost, endpoint, data) - if err != nil { - return nil, err - } - - result := &AddRecordResponse{} - - err = c.do(req, result) - if err != nil { - return nil, err - } - - return result.Record, nil -} - -// DeleteDNSRecord deletes a DNS record. -// https://octenium.com/api#tag/Domains-DNS/operation/domains-dns-records-delete -func (c *Client) DeleteDNSRecord(ctx context.Context, orderID string, recordID int) (*DeletedRecordInfo, error) { - endpoint := c.BaseURL.JoinPath("domains", "dns-records", "delete") - - data := make(url.Values) - data.Set("order-id", orderID) - data.Set("line", strconv.Itoa(recordID)) - - req, err := newRequest(ctx, http.MethodPost, endpoint, data) - if err != nil { - return nil, err - } - - result := &DeleteRecordResponse{} - - err = c.do(req, result) - if err != nil { - return nil, err - } - - return result.Deleted, nil -} - -func (c *Client) do(req *http.Request, result any) error { - req.Header.Set("X-Api-Key", c.apiKey) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - raw, _ := io.ReadAll(resp.Body) - - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - var response APIResponse - - err = json.Unmarshal(raw, &response) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - if response.Status != statusSuccess { - return fmt.Errorf("unexpected status: %s: %s", response.Status, response.Error) - } - - err = json.Unmarshal(response.Response, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, response.Response, err) - } - - return nil -} - -func newRequest(ctx context.Context, method string, endpoint *url.URL, payload url.Values) (*http.Request, error) { - var body io.Reader = http.NoBody - - if method == http.MethodPost && payload != nil { - body = strings.NewReader(payload.Encode()) - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if method == http.MethodPost && payload != nil { - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - } - - return req, nil -} diff --git a/providers/dns/octenium/internal/client_test.go b/providers/dns/octenium/internal/client_test.go deleted file mode 100644 index ff1b21961..000000000 --- a/providers/dns/octenium/internal/client_test.go +++ /dev/null @@ -1,224 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("secret") - if err != nil { - return nil, err - } - - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader(). - WithAccept("application/json"). - With("X-Api-Key", "secret"), - ) -} - -func TestClient_ListDomains(t *testing.T) { - client := mockBuilder(). - Route("GET /domains", - servermock.ResponseFromFixture("list_domains.json"), - servermock.CheckQueryParameter().Strict(). - With("domain-name", "example.com")). - Build(t) - - domains, err := client.ListDomains(t.Context(), "example.com") - require.NoError(t, err) - - expected := map[string]Domain{ - "2976": {DomainName: "example.com", RegistrationDate: "12/09/2021", ExpirationDate: "12/09/2024", Status: "active"}, - "2977": {DomainName: "example.org", RegistrationDate: "01/10/2021", ExpirationDate: "01/10/2024", Status: "active"}, - "2978": {DomainName: "example.net", RegistrationDate: "21/08/2025", ExpirationDate: "-", Status: "active"}, - } - - assert.Equal(t, expected, domains) -} - -func TestClient_ListDomains_error(t *testing.T) { - client := mockBuilder(). - Route("GET /domains", - servermock.Noop().WithStatusCode(http.StatusBadRequest)). - Build(t) - - _, err := client.ListDomains(t.Context(), "example.com") - require.EqualError(t, err, "unexpected status code: [status code: 400] body: ") -} - -func TestClient_ListDomains_api_error(t *testing.T) { - client := mockBuilder(). - Route("GET /domains", - servermock.ResponseFromFixture("error.json")). - Build(t) - - _, err := client.ListDomains(t.Context(), "example.com") - require.EqualError(t, err, "unexpected status: error: missing required fields (type, name, ttl)") -} - -func TestClient_ListDNSRecords(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/dns-records/list", - servermock.ResponseFromFixture("list_dns_records.json"), - servermock.CheckHeader(). - WithContentType("application/x-www-form-urlencoded"), - servermock.CheckForm().Strict(). - With("order-id", "abc"). - With("types[]", "TXT")). - Build(t) - - records, err := client.ListDNSRecords(t.Context(), "abc", "TXT") - require.NoError(t, err) - - expected := []Record{ - {ID: 15, Type: "A", Name: "example.com.", TTL: 14400, Value: "203.0.113.10"}, - {ID: 22, Type: "MX", Name: "example.com.", TTL: 14400, Value: "10 mail.example.com."}, - {ID: 31, Type: "TXT", Name: "_dmarc.example.com.", TTL: 300, Value: "v=DMARC1; p=none; rua=mailto:dmarc@example.com"}, - } - - assert.Equal(t, expected, records) -} - -func TestClient_ListDNSRecords_error(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/dns-records/list", - servermock.Noop().WithStatusCode(http.StatusBadRequest)). - Build(t) - - _, err := client.ListDNSRecords(t.Context(), "abc", "TXT") - require.EqualError(t, err, "unexpected status code: [status code: 400] body: ") -} - -func TestClient_ListDNSRecords_api_error(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/dns-records/list", - servermock.ResponseFromFixture("error.json")). - Build(t) - - _, err := client.ListDNSRecords(t.Context(), "abc", "TXT") - require.EqualError(t, err, "unexpected status: error: missing required fields (type, name, ttl)") -} - -func TestClient_AddDNSRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/dns-records/add", - servermock.ResponseFromFixture("add_dns_record.json"), - servermock.CheckHeader(). - WithContentType("application/x-www-form-urlencoded"), - servermock.CheckForm().Strict(). - With("order-id", "abc"). - With("name", "example.com."). - With("ttl", "120"). - With("type", "TXT"). - With("value", "txtTXTtxt")). - Build(t) - - record := Record{ - Type: "TXT", - Name: "example.com.", - TTL: 120, - Value: "txtTXTtxt", - } - - result, err := client.AddDNSRecord(t.Context(), "abc", record) - require.NoError(t, err) - - expected := &Record{ - Type: "A", - Name: "example.com.", - TTL: 14400, - Value: "203.0.113.10", - } - - assert.Equal(t, expected, result) -} - -func TestClient_AddDNSRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/dns-records/add", - servermock.Noop().WithStatusCode(http.StatusBadRequest)). - Build(t) - - record := Record{ - Type: "TXT", - Name: "example.com.", - TTL: 120, - Value: "txtTXTtxt", - } - - _, err := client.AddDNSRecord(t.Context(), "abc", record) - require.EqualError(t, err, "unexpected status code: [status code: 400] body: ") -} - -func TestClient_AddDNSRecord_api_error(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/dns-records/add", - servermock.ResponseFromFixture("error.json")). - Build(t) - - record := Record{ - Type: "TXT", - Name: "example.com.", - TTL: 120, - Value: "txtTXTtxt", - } - - _, err := client.AddDNSRecord(t.Context(), "abc", record) - require.EqualError(t, err, "unexpected status: error: missing required fields (type, name, ttl)") -} - -func TestClient_DeleteDNSRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/dns-records/delete", - servermock.ResponseFromFixture("delete_dns_record.json"), - servermock.CheckHeader(). - WithContentType("application/x-www-form-urlencoded"), - servermock.CheckForm().Strict(). - With("order-id", "abc"). - With("line", "123")). - Build(t) - - result, err := client.DeleteDNSRecord(t.Context(), "abc", 123) - require.NoError(t, err) - - expected := &DeletedRecordInfo{ - Count: 1, - Lines: []int{15}, - } - - assert.Equal(t, expected, result) -} - -func TestClient_DeleteDNSRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/dns-records/delete", - servermock.Noop().WithStatusCode(http.StatusBadRequest)). - Build(t) - - _, err := client.DeleteDNSRecord(t.Context(), "abc", 123) - require.EqualError(t, err, "unexpected status code: [status code: 400] body: ") -} - -func TestClient_DeleteDNSRecord_api_error(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/dns-records/delete", - servermock.ResponseFromFixture("error.json")). - Build(t) - - _, err := client.DeleteDNSRecord(t.Context(), "abc", 123) - require.EqualError(t, err, "unexpected status: error: missing required fields (type, name, ttl)") -} diff --git a/providers/dns/octenium/internal/fixtures/add_dns_record.json b/providers/dns/octenium/internal/fixtures/add_dns_record.json deleted file mode 100644 index 6c73ea1f9..000000000 --- a/providers/dns/octenium/internal/fixtures/add_dns_record.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "api-status": "success", - "api-response": { - "record": { - "type": "A", - "name": "example.com.", - "ttl": 14400, - "value": "203.0.113.10", - "raw": { - "address": "203.0.113.10" - } - } - } -} diff --git a/providers/dns/octenium/internal/fixtures/delete_dns_record.json b/providers/dns/octenium/internal/fixtures/delete_dns_record.json deleted file mode 100644 index 0d4692ffd..000000000 --- a/providers/dns/octenium/internal/fixtures/delete_dns_record.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "api-status": "success", - "api-response": { - "deleted": { - "count": 1, - "lines": [ - 15 - ] - } - } -} diff --git a/providers/dns/octenium/internal/fixtures/error.json b/providers/dns/octenium/internal/fixtures/error.json deleted file mode 100644 index 85a90e425..000000000 --- a/providers/dns/octenium/internal/fixtures/error.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "api-status": "error", - "api-response": [], - "api-error": "missing required fields (type, name, ttl)" -} diff --git a/providers/dns/octenium/internal/fixtures/list_dns_records.json b/providers/dns/octenium/internal/fixtures/list_dns_records.json deleted file mode 100644 index 8fa60d86f..000000000 --- a/providers/dns/octenium/internal/fixtures/list_dns_records.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "api-status": "success", - "api-response": { - "records": [ - { - "line": 15, - "type": "A", - "name": "example.com.", - "ttl": 14400, - "value": "203.0.113.10", - "raw": { - "address": "203.0.113.10" - } - }, - { - "line": 22, - "type": "MX", - "name": "example.com.", - "ttl": 14400, - "value": "10 mail.example.com.", - "raw": { - "preference": 10, - "exchange": "mail.example.com." - } - }, - { - "line": 31, - "type": "TXT", - "name": "_dmarc.example.com.", - "ttl": 300, - "value": "v=DMARC1; p=none; rua=mailto:dmarc@example.com", - "raw": { - "txtdata": "v=DMARC1; p=none; rua=mailto:dmarc@example.com" - } - } - ] - } -} diff --git a/providers/dns/octenium/internal/fixtures/list_domains.json b/providers/dns/octenium/internal/fixtures/list_domains.json deleted file mode 100644 index b10b705c9..000000000 --- a/providers/dns/octenium/internal/fixtures/list_domains.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "api-status": "success", - "api-response": { - "domains": { - "2976": { - "domain-name": "example.com", - "registration-date": "12/09/2021", - "expiration-date": "12/09/2024", - "status": "active" - }, - "2977": { - "domain-name": "example.org", - "registration-date": "01/10/2021", - "expiration-date": "01/10/2024", - "status": "active" - }, - "2978": { - "domain-name": "example.net", - "registration-date": "21\/08\/2025", - "expiration-date": "-", - "status": "active" - } - } - } -} diff --git a/providers/dns/octenium/internal/types.go b/providers/dns/octenium/internal/types.go deleted file mode 100644 index a31e40921..000000000 --- a/providers/dns/octenium/internal/types.go +++ /dev/null @@ -1,45 +0,0 @@ -package internal - -import "encoding/json" - -type APIResponse struct { - Status string `json:"api-status,omitempty"` - Response json.RawMessage `json:"api-response,omitempty"` - Error string `json:"api-error,omitempty"` -} - -type Domain struct { - DomainName string `json:"domain-name,omitempty"` - RegistrationDate string `json:"registration-date,omitempty"` - ExpirationDate string `json:"expiration-date,omitempty"` - Status string `json:"status,omitempty"` -} - -type Record struct { - ID int `json:"line,omitempty" url:"-"` - Type string `json:"type,omitempty" url:"type,omitempty"` - Name string `json:"name,omitempty" url:"name,omitempty"` - TTL int `json:"ttl,omitempty" url:"ttl,omitempty"` - Value string `json:"value,omitempty" url:"value,omitempty"` -} - -type DomainsResponse struct { - Domains map[string]Domain `json:"domains,omitempty"` -} - -type AddRecordResponse struct { - Record *Record `json:"record,omitempty"` -} - -type ListRecordsResponse struct { - Records []Record `json:"records,omitempty"` -} - -type DeleteRecordResponse struct { - Deleted *DeletedRecordInfo `json:"deleted,omitempty"` -} - -type DeletedRecordInfo struct { - Count int `json:"count,omitempty"` - Lines []int `json:"lines,omitempty"` -} diff --git a/providers/dns/octenium/octenium.go b/providers/dns/octenium/octenium.go deleted file mode 100644 index 6032dcce1..000000000 --- a/providers/dns/octenium/octenium.go +++ /dev/null @@ -1,204 +0,0 @@ -// Package octenium implements a DNS provider for solving the DNS-01 challenge using Octenium. -package octenium - -import ( - "context" - "errors" - "fmt" - "net/http" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/log" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/octenium/internal" - "github.com/hashicorp/go-retryablehttp" -) - -// Environment variables names. -const ( - envNamespace = "OCTENIUM_" - - EnvAPIKey = envNamespace + "API_KEY" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client - - domainIDs map[string]string - domainIDsMu sync.Mutex -} - -// NewDNSProvider returns a DNSProvider instance configured for Octenium. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIKey) - if err != nil { - return nil, fmt.Errorf("octenium: %w", err) - } - - config := NewDefaultConfig() - config.APIKey = values[EnvAPIKey] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Octenium. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("octenium: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.APIKey) - if err != nil { - return nil, fmt.Errorf("octenium: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - retryClient := retryablehttp.NewClient() - retryClient.RetryMax = 5 - retryClient.HTTPClient = client.HTTPClient - retryClient.Logger = log.Logger - - client.HTTPClient = clientdebug.Wrap(retryClient.StandardClient()) - - return &DNSProvider{ - config: config, - client: client, - domainIDs: make(map[string]string), - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("octenium: could not find zone for domain '%s': %w", domain, err) - } - - domainID, err := d.getDomainID(ctx, authZone) - if err != nil { - return fmt.Errorf("octenium: get domain ID: %w", err) - } - - d.domainIDsMu.Lock() - d.domainIDs[token] = domainID - d.domainIDsMu.Unlock() - - record := internal.Record{ - Type: "TXT", - Name: info.EffectiveFQDN, - TTL: d.config.TTL, - Value: info.Value, - } - - _, err = d.client.AddDNSRecord(ctx, domainID, record) - if err != nil { - return fmt.Errorf("octenium: add record: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - d.domainIDsMu.Lock() - domainID, ok := d.domainIDs[token] - d.domainIDsMu.Unlock() - - if !ok { - return fmt.Errorf("octenium: unknown domain ID for '%s'", info.EffectiveFQDN) - } - - records, err := d.client.ListDNSRecords(ctx, domainID, "TXT") - if err != nil { - return fmt.Errorf("octenium: list records: %w", err) - } - - for _, record := range records { - if record.Type != "TXT" || record.Name != info.EffectiveFQDN || record.Value != info.Value { - continue - } - - _, err = d.client.DeleteDNSRecord(ctx, domainID, record.ID) - if err != nil { - return fmt.Errorf("octenium: delete record: %w", err) - } - - break - } - - d.domainIDsMu.Lock() - delete(d.domainIDs, token) - d.domainIDsMu.Unlock() - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -func (d *DNSProvider) getDomainID(ctx context.Context, authZone string) (string, error) { - domains, err := d.client.ListDomains(ctx, dns01.UnFqdn(authZone)) - if err != nil { - return "", fmt.Errorf("list domains: %w", err) - } - - if len(domains) == 0 { - return "", errors.New("domain not found") - } - - if len(domains) > 1 { - return "", errors.New("multiple domains found") - } - - for id := range domains { - return id, nil - } - - return "", errors.New("domain ID not found") -} diff --git a/providers/dns/octenium/octenium.toml b/providers/dns/octenium/octenium.toml deleted file mode 100644 index e3c9d894f..000000000 --- a/providers/dns/octenium/octenium.toml +++ /dev/null @@ -1,22 +0,0 @@ -Name = "Octenium" -Description = '''''' -URL = "https://octenium.com/" -Code = "octenium" -Since = "v4.27.0" - -Example = ''' -OCTENIUM_API_KEY="xxx" \ -lego --dns octenium -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - OCTENIUM_API_KEY = "API key" - [Configuration.Additional] - OCTENIUM_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - OCTENIUM_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - OCTENIUM_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - OCTENIUM_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://octenium.com/api#tag/Domains-DNS" diff --git a/providers/dns/octenium/octenium_test.go b/providers/dns/octenium/octenium_test.go deleted file mode 100644 index dbb8d64b3..000000000 --- a/providers/dns/octenium/octenium_test.go +++ /dev/null @@ -1,198 +0,0 @@ -package octenium - -import ( - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAPIKey: "secret", - }, - }, - { - desc: "missing API key", - envVars: map[string]string{ - EnvAPIKey: "", - }, - expected: "octenium: some credentials information are missing: OCTENIUM_API_KEY", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "octenium: some credentials information are missing: OCTENIUM_API_KEY", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiKey string - expected string - }{ - { - desc: "success", - apiKey: "secret", - }, - { - desc: "missing API key", - expected: "octenium: credentials missing", - }, - { - desc: "missing credentials", - expected: "octenium: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIKey = test.apiKey - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIKey = "secret" - config.HTTPClient = server.Client() - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - p.client.BaseURL, _ = url.Parse(server.URL) - - return p, nil - }, - servermock.CheckHeader(). - WithAccept("application/json"). - With("X-Api-Key", "secret"), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("GET /domains", - servermock.ResponseFromFixture("list_domains.json"), - servermock.CheckQueryParameter().Strict(). - With("domain-name", "example.com")). - Route("POST /domains/dns-records/add", - servermock.ResponseFromFixture("add_dns_record.json"), - servermock.CheckHeader(). - WithContentType("application/x-www-form-urlencoded"), - servermock.CheckForm().Strict(). - With("order-id", "2976"). - With("name", "_acme-challenge.example.com."). - With("ttl", "120"). - With("type", "TXT"). - With("value", "w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI")). - Build(t) - - err := provider.Present("example.com", "", "foobar") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("POST /domains/dns-records/list", - servermock.ResponseFromFixture("list_dns_records.json"), - servermock.CheckHeader(). - WithContentType("application/x-www-form-urlencoded"), - servermock.CheckForm().Strict(). - With("order-id", "2976"). - With("types[]", "TXT")). - Route("POST /domains/dns-records/delete", - servermock.ResponseFromFixture("delete_dns_record.json"), - servermock.CheckHeader(). - WithContentType("application/x-www-form-urlencoded"), - servermock.CheckForm().Strict(). - With("order-id", "2976"). - With("line", "123")). - Build(t) - - provider.domainIDs["token"] = "2976" - - err := provider.CleanUp("example.com", "token", "foobar") - require.NoError(t, err) -} diff --git a/providers/dns/oraclecloud/configprovider.go b/providers/dns/oraclecloud/configprovider.go new file mode 100644 index 000000000..43d0cecc3 --- /dev/null +++ b/providers/dns/oraclecloud/configprovider.go @@ -0,0 +1,97 @@ +package oraclecloud + +import ( + "crypto/rsa" + "encoding/base64" + "errors" + "fmt" + "os" + + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/oracle/oci-go-sdk/v65/common" +) + +type configProvider struct { + values map[string]string + privateKeyPassphrase string +} + +func newConfigProvider(values map[string]string) *configProvider { + return &configProvider{ + values: values, + privateKeyPassphrase: env.GetOrFile(EnvPrivKeyPass), + } +} + +func (p *configProvider) PrivateRSAKey() (*rsa.PrivateKey, error) { + privateKey, err := getPrivateKey(envPrivKey) + if err != nil { + return nil, err + } + + return common.PrivateKeyFromBytesWithPassword(privateKey, []byte(p.privateKeyPassphrase)) +} + +func (p *configProvider) KeyID() (string, error) { + tenancy, err := p.TenancyOCID() + if err != nil { + return "", err + } + + user, err := p.UserOCID() + if err != nil { + return "", err + } + + fingerprint, err := p.KeyFingerprint() + if err != nil { + return "", err + } + + return fmt.Sprintf("%s/%s/%s", tenancy, user, fingerprint), nil +} + +func (p *configProvider) TenancyOCID() (value string, err error) { + return p.values[EnvTenancyOCID], nil +} + +func (p *configProvider) UserOCID() (string, error) { + return p.values[EnvUserOCID], nil +} + +func (p *configProvider) KeyFingerprint() (string, error) { + return p.values[EnvPubKeyFingerprint], nil +} + +func (p *configProvider) Region() (string, error) { + return p.values[EnvRegion], nil +} + +func (p *configProvider) AuthType() (common.AuthConfig, error) { + // Inspired by https://github.com/oracle/oci-go-sdk/blob/e7635c292e60d0a9dcdd3a1e7de180d7c99b1eee/common/configuration.go#L231-L234 + return common.AuthConfig{AuthType: common.UnknownAuthenticationType}, errors.New("unsupported, keep the interface") +} + +func getPrivateKey(envVar string) ([]byte, error) { + envVarValue := os.Getenv(envVar) + if envVarValue != "" { + bytes, err := base64.StdEncoding.DecodeString(envVarValue) + if err != nil { + return nil, fmt.Errorf("failed to read base64 value %s (defined by env var %s): %w", envVarValue, envVar, err) + } + return bytes, nil + } + + fileVar := envVar + "_FILE" + fileVarValue := os.Getenv(fileVar) + if fileVarValue == "" { + return nil, fmt.Errorf("no value provided for: %s or %s", envVar, fileVar) + } + + fileContents, err := os.ReadFile(fileVarValue) + if err != nil { + return nil, fmt.Errorf("failed to read the file %s (defined by env var %s): %w", fileVarValue, fileVar, err) + } + + return fileContents, nil +} diff --git a/providers/dns/oraclecloud/configurationprovider.go b/providers/dns/oraclecloud/configurationprovider.go deleted file mode 100644 index 97710108c..000000000 --- a/providers/dns/oraclecloud/configurationprovider.go +++ /dev/null @@ -1,144 +0,0 @@ -package oraclecloud - -import ( - "crypto/rsa" - "encoding/base64" - "errors" - "fmt" - "os" - "slices" - "strings" - - "github.com/go-acme/lego/v4/log" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/nrdcg/oci-go-sdk/common/v1065" -) - -type environmentConfigurationProvider struct { - values map[string]string -} - -func newEnvironmentConfigurationProvider() (*environmentConfigurationProvider, error) { - values, err := env.GetWithFallback( - []string{EnvRegion, altEnvTFVarRegion}, - []string{EnvUserOCID, altEnvTFVarUserOCID}, - []string{EnvTenancyOCID, altEnvTFVarTenancyOCID}, - []string{EnvPubKeyFingerprint, altEnvFingerprint, altEnvTFVarFingerprint}, - ) - if err != nil { - return nil, err - } - - return &environmentConfigurationProvider{ - values: values, - }, nil -} - -func (p *environmentConfigurationProvider) PrivateRSAKey() (*rsa.PrivateKey, error) { - privateKey, err := getPrivateKey() - if err != nil { - return nil, err - } - - return common.PrivateKeyFromBytesWithPassword(privateKey, []byte(p.privateKeyPassword())) -} - -func (p *environmentConfigurationProvider) KeyID() (string, error) { - tenancy, err := p.TenancyOCID() - if err != nil { - return "", err - } - - user, err := p.UserOCID() - if err != nil { - return "", err - } - - fingerprint, err := p.KeyFingerprint() - if err != nil { - return "", err - } - - return fmt.Sprintf("%s/%s/%s", tenancy, user, fingerprint), nil -} - -func (p *environmentConfigurationProvider) TenancyOCID() (string, error) { - return p.values[EnvTenancyOCID], nil -} - -func (p *environmentConfigurationProvider) UserOCID() (string, error) { - return p.values[EnvUserOCID], nil -} - -func (p *environmentConfigurationProvider) KeyFingerprint() (string, error) { - return p.values[EnvPubKeyFingerprint], nil -} - -func (p *environmentConfigurationProvider) Region() (string, error) { - return p.values[EnvRegion], nil -} - -func (p *environmentConfigurationProvider) AuthType() (common.AuthConfig, error) { - // Inspired by https://github.com/oracle/oci-go-sdk/blob/e7635c292e60d0a9dcdd3a1e7de180d7c99b1eee/common/configuration.go#L231-L234 - return common.AuthConfig{AuthType: common.UnknownAuthenticationType}, errors.New("unsupported, keep the interface") -} - -func (p *environmentConfigurationProvider) privateKeyPassword() string { - return env.GetOneWithFallback(EnvPrivKeyPass, "", env.ParseString, altEnvPrivateKeyPassword, altEnvTFVarPrivateKeyPassword) -} - -func getPrivateKey() ([]byte, error) { - base64EnvKeys := []string{envPrivKey, altEnvPrivateKey} - - envVarValue := getEnvWithStrictFallback(base64EnvKeys...) - if envVarValue != "" { - bytes, err := base64.StdEncoding.DecodeString(envVarValue) - if err != nil { - return nil, fmt.Errorf("failed to read base64 value %s (defined by env vars %s): %w", envVarValue, - strings.Join(base64EnvKeys, " or "), err) - } - - return bytes, nil - } - - fileEnvKeys := []string{EnvPrivKeyFile, altEnvPrivateKeyPath, altEnvTFVarPrivateKeyPath} - - fileVarValue := getEnvFileWithStrictFallback(fileEnvKeys...) - if len(fileVarValue) == 0 { - return nil, fmt.Errorf("no value provided for: %s", - strings.Join(slices.Concat(base64EnvKeys, fileEnvKeys), " or "), - ) - } - - return fileVarValue, nil -} - -func getEnvWithStrictFallback(keys ...string) string { - for _, key := range keys { - envVarValue := os.Getenv(key) - if envVarValue != "" { - return envVarValue - } - } - - return "" -} - -func getEnvFileWithStrictFallback(keys ...string) []byte { - for _, key := range keys { - fileVarValue := os.Getenv(key) - if fileVarValue == "" { - continue - } - - fileContents, err := os.ReadFile(fileVarValue) - if err != nil { - log.Printf("Failed to read the file %s (defined by env var %s): %s", fileVarValue, key, err) - return nil - } - - return fileContents - } - - return nil -} diff --git a/providers/dns/oraclecloud/fixtures/cert.pem b/providers/dns/oraclecloud/fixtures/cert.pem deleted file mode 100644 index fc1dcfb53..000000000 --- a/providers/dns/oraclecloud/fixtures/cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDHzCCAgegAwIBAgIQKIExaCLIXtXecrT1dWGLszANBgkqhkiG9w0BAQsFADAS -MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw -MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAwM4wEPHOGAu8tZNNWx3cH6AMuqKwAmB2RwbA3OK034MzhydOjnDm -igw93eUc4nd3dnICyNpb2rbP9FgGlAuMlJ8raHQkG4DSXF1Bf14neOhLpfBItaX9 -+EB3oO0NupKZhaHrsTKzLGD7bauAPX6PDXuAPp3u5mgGGuZjpLZoKqg3//WImb/2 -xEMVsmvPKTb5FxS/tAMtywjGSUtCTCrudUEh4Gnj6IboVdwYmt539ETDK/Rerxf3 -/GsmEbuOkDUdBixQwLo0U+UAoMOw4zoyQDrrtyUmvffDxI50RAdZDFyFtqZ0ZQa8 -lQqrMdQdf+x1Wb7BKozSktAw4igRP/mknQIDAQABo28wbTAOBgNVHQ8BAf8EBAMC -AqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E -FgQUcetTliVbYxxutNS8JRkotRY4DRkwFgYDVR0RBA8wDYILZXhhbXBsZS5vcmcw -DQYJKoZIhvcNAQELBQADggEBAEJP74/XB+12aGQ+EMERIX2Pn6YaaBLt6rTLqV7A -zFxI9YGIc4xlGa0qkpDhpz6RSypTQG6HN5aZ5b8dz3foMleUVP2cXd8zduc8GQCb -p4/8PpEhSl6dQb5+mg/qyHGUAaDl40VAbTLXHtn98dhacaJc+TKuXVJAgYRU3Sm3 -wFJxULZSnx+aGdE9s2brOGhvz1fVWnhvWzDvJSM+8xDURz8UiEnimTpV6m3CKItz -2GatNjM8ADKC7MHQI4I5v4fEwronN/g3NfPfFSmnOKk+lPSAW42WEvhFol+2VvdX -3p5X2QracSLCIj/DUBebZP9110C8Lj/YfFtOjFokqtQ9Fh4= ------END CERTIFICATE----- diff --git a/providers/dns/oraclecloud/fixtures/key.pem b/providers/dns/oraclecloud/fixtures/key.pem deleted file mode 100644 index 1a56bb5a4..000000000 --- a/providers/dns/oraclecloud/fixtures/key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDAzjAQ8c4YC7y1 -k01bHdwfoAy6orACYHZHBsDc4rTfgzOHJ06OcOaKDD3d5Rzid3d2cgLI2lvats/0 -WAaUC4yUnytodCQbgNJcXUF/Xid46Eul8Ei1pf34QHeg7Q26kpmFoeuxMrMsYPtt -q4A9fo8Ne4A+ne7maAYa5mOktmgqqDf/9YiZv/bEQxWya88pNvkXFL+0Ay3LCMZJ -S0JMKu51QSHgaePohuhV3Bia3nf0RMMr9F6vF/f8ayYRu46QNR0GLFDAujRT5QCg -w7DjOjJAOuu3JSa998PEjnREB1kMXIW2pnRlBryVCqsx1B1/7HVZvsEqjNKS0DDi -KBE/+aSdAgMBAAECggEAWl2pWJ/ErS9/HIl0NbMKk0YEAUuz/AEzHnoTVdPp22KW -eY+aOZe/7c7sBj7WqWw98SVhmbsCV0HcuNSzDJtXIedyRGw+6icYMVNCGgzKqlgR -8K3snjq1DLBGgYXpq9r/Got4ON6e7LttzIqXufrB2JtcUbzbFmGGDwCRjkcyDl9l -M8ufwD/Xgcd2L8jainU43d2pVxvxUIpRlRdoupCCSlkRYPsXiWlqav7YO4F/Txos -z3gJyzkXzc3WwfNZdQtEMYwBwozO+Dp2p4TUBr0Ta3MbfrKfDoTs4XT/Ce9IwJJS -/h6E9cxZD8t5oMT50quFjwhHBKodMiUqIlh2YQEAbwKBgQDIULzo/tgDgTwveyEn -L9n8yVbEh/SfrE9QtXcjkDB5+tYmIsIaz16NRWlAqnJVGZvcanrCq7ZTxgUcs/hW -Ag+sfWkeg7lmfeJAkiZ6kmi1h2qJjXMOBri+Cm6MTOsE6qdIc3eT4PnYkNpV7o6S -70hWNncVadXLV4Thm9BLAbMbQwKBgQD2ZwKe/2zRQcbuBe1loF0HWIsJPxcKQ3LH -hVf7f0YLQlIuzOhK8TQXgM0G4hxLlk1XeLjgf3z4Ju7hfh2JQLor1QYPRGUj66SX -KTE5eDwE0yEX1c9m5PW6M+f8vkOU4LQ/OtPw5OrKyYxpLf9dp42nmDYY/8IvUk96 -iKZNY1sSnwKBgQC27tS2SxVmjf0yt1WdfdurOQueSzKhJzD/2djFh4Zdvy8WgKOW -7E3C4eKvBXmIMezeq/cUFNBbTPmaLtjZYuSBd74p+c20xb17jnzJby9kqBgpKh4q -bwUDuG8gfZYbVVgTmC9ZwxkoJ5Dc7RETKqZ65R53VcHDA1f82Nitxw2UFQKBgBDl -c2qPvViEGC4OPf8wBfERA0e5Cc1sXpyL6kKWsajn/Va0OmGZNKc/788/Bg2w2tDa -uGK8m0cw9ESGL2RQCfQjgWzelcjmybyL2JJGSmdSSvylbrlxjeAc2xWbvmqhFfsX -/5yPNgJ926ECxHYZnT8W0u7X6urvy/9tC2pXG9GlAoGBAKOAfij4fMbHY+Z1m825 -VhY110FDnePYFJWmExP8GAVqOzhCs0mzyCnYh6nvS/OY8moH2LOuwPUlDfF3IzyT -hTUuXnykWT3w40eYQXXIaXEGhue+guL8ch16vEEJy5ltwEdIPNMTErbqAAk2W6Ps -NB46HzETzEIWnzoamX6iQVWj ------END PRIVATE KEY----- diff --git a/providers/dns/oraclecloud/oraclecloud.go b/providers/dns/oraclecloud/oraclecloud.go index 730b3f212..a55613810 100644 --- a/providers/dns/oraclecloud/oraclecloud.go +++ b/providers/dns/oraclecloud/oraclecloud.go @@ -8,35 +8,24 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/nrdcg/oci-go-sdk/common/v1065" - "github.com/nrdcg/oci-go-sdk/common/v1065/auth" - "github.com/nrdcg/oci-go-sdk/dns/v1065" + "github.com/oracle/oci-go-sdk/v65/common" + "github.com/oracle/oci-go-sdk/v65/dns" ) // Environment variables names. const ( envNamespace = "OCI_" - EnvAuthType = envNamespace + "AUTH_TYPE" - - EnvCompartmentOCID = envNamespace + "COMPARTMENT_OCID" - EnvRegion = envNamespace + "REGION" - + EnvCompartmentOCID = envNamespace + "COMPARTMENT_OCID" envPrivKey = envNamespace + "PRIVKEY" EnvPrivKeyFile = envPrivKey + "_FILE" EnvPrivKeyPass = envPrivKey + "_PASS" EnvTenancyOCID = envNamespace + "TENANCY_OCID" EnvUserOCID = envNamespace + "USER_OCID" EnvPubKeyFingerprint = envNamespace + "PUBKEY_FINGERPRINT" - - altEnvPrivateKey = envNamespace + "PRIVATE_KEY" // alias on OCI_PRIVKEY - altEnvPrivateKeyPath = altEnvPrivateKey + "_PATH" // alias on OCI_PRIVKEY_FILE - altEnvPrivateKeyPassword = altEnvPrivateKey + "_PASSWORD" // alias on OCI_PRIVKEY_PASS - altEnvFingerprint = envNamespace + "FINGERPRINT" // alias on OCI_PUBKEY_FINGERPRINT + EnvRegion = envNamespace + "REGION" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" @@ -44,25 +33,10 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -// https://github.com/oracle/oci-go-sdk/blob/7f425f74c74fd0c6a5acb74466c85eb5346e0092/common/client.go#L350 -// https://github.com/oracle/oci-go-sdk/blob/7f425f74c74fd0c6a5acb74466c85eb5346e0092/common/configuration.go#L174-L175 -const ( - altEnvTFVarNamespace = "TF_VAR_" - altEnvTFVarRegion = altEnvTFVarNamespace + "region" // alias on OCI_REGION - altEnvTFVarFingerprint = altEnvTFVarNamespace + "fingerprint" // alias on OCI_PUBKEY_FINGERPRINT - altEnvTFVarUserOCID = altEnvTFVarNamespace + "user_ocid" // alias on OCI_USER_OCID - altEnvTFVarTenancyOCID = altEnvTFVarNamespace + "tenancy_ocid" // alias on OCI_TENANCY_OCID - altEnvTFVarPrivateKeyPath = altEnvTFVarNamespace + "private_key_path" // alias on OCI_PRIVKEY_FILE - altEnvTFVarPrivateKeyPassword = altEnvTFVarNamespace + "private_key_password" // alias on OCI_PRIVKEY_PASS -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { - CompartmentID string - OCIConfigProvider common.ConfigurationProvider - + CompartmentID string + OCIConfigProvider common.ConfigurationProvider PropagationTimeout time.Duration PollingInterval time.Duration TTL int @@ -76,7 +50,7 @@ func NewDefaultConfig() *Config { PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, time.Minute), + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 60*time.Second), }, } } @@ -89,42 +63,15 @@ type DNSProvider struct { // NewDNSProvider returns a DNSProvider instance configured for OracleCloud. func NewDNSProvider() (*DNSProvider, error) { - config := NewDefaultConfig() - - switch env.GetOrFile(EnvAuthType) { - case string(common.InstancePrincipal): - values, err := env.Get(EnvCompartmentOCID) - if err != nil { - return nil, fmt.Errorf("oraclecloud: %w", err) - } - - config.CompartmentID = values[EnvCompartmentOCID] - - region := env.GetOneWithFallback(EnvRegion, "", env.ParseString, altEnvTFVarRegion) - - configurationProvider, err := auth.InstancePrincipalConfigurationProviderForRegion(common.Region(region)) - if err != nil { - return nil, fmt.Errorf("oraclecloud: %w", err) - } - - config.OCIConfigProvider = configurationProvider - - default: - values, err := env.Get(EnvCompartmentOCID) - if err != nil { - return nil, fmt.Errorf("oraclecloud: %w", err) - } - - config.CompartmentID = values[EnvCompartmentOCID] - - ecp, err := newEnvironmentConfigurationProvider() - if err != nil { - return nil, fmt.Errorf("oraclecloud: %w", err) - } - - config.OCIConfigProvider = ecp + values, err := env.Get(envPrivKey, EnvTenancyOCID, EnvUserOCID, EnvPubKeyFingerprint, EnvRegion, EnvCompartmentOCID) + if err != nil { + return nil, fmt.Errorf("oraclecloud: %w", err) } + config := NewDefaultConfig() + config.CompartmentID = values[EnvCompartmentOCID] + config.OCIConfigProvider = newConfigProvider(values) + return NewDNSProviderConfig(config) } @@ -148,7 +95,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { } if config.HTTPClient != nil { - client.HTTPClient = clientdebug.Wrap(config.HTTPClient) + client.HTTPClient = config.HTTPClient } return &DNSProvider{client: &client, config: config}, nil @@ -218,8 +165,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } var deleteHash *string - - for _, record := range domainRecords.Items { + for _, record := range domainRecords.RecordCollection.Items { if record.Rdata != nil && *record.Rdata == `"`+info.Value+`"` { deleteHash = record.RecordHash break diff --git a/providers/dns/oraclecloud/oraclecloud.toml b/providers/dns/oraclecloud/oraclecloud.toml index f6155052e..9380be9ed 100644 --- a/providers/dns/oraclecloud/oraclecloud.toml +++ b/providers/dns/oraclecloud/oraclecloud.toml @@ -5,43 +5,29 @@ Code = "oraclecloud" Since = "v2.3.0" Example = ''' -# Using API Key authentication: -OCI_PRIVATE_KEY_PATH="~/.oci/oci_api_key.pem" \ -OCI_PRIVATE_KEY_PASSWORD="secret" \ +OCI_PRIVKEY_FILE="~/.oci/oci_api_key.pem" \ +OCI_PRIVKEY_PASS="secret" \ OCI_TENANCY_OCID="ocid1.tenancy.oc1..secret" \ OCI_USER_OCID="ocid1.user.oc1..secret" \ -OCI_FINGERPRINT="00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00" \ +OCI_PUBKEY_FINGERPRINT="00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00" \ OCI_REGION="us-phoenix-1" \ OCI_COMPARTMENT_OCID="ocid1.tenancy.oc1..secret" \ -lego --dns oraclecloud -d '*.example.com' -d example.com run - -# Using Instance Principal authentication (when running on OCI compute instances): -# https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm -OCI_AUTH_TYPE="instance_principal" \ -OCI_COMPARTMENT_OCID="ocid1.tenancy.oc1..secret" \ -lego --dns oraclecloud -d '*.example.com' -d example.com run +lego --email you@example.com --dns oraclecloud --domains my.example.org run ''' [Configuration] [Configuration.Credentials] + OCI_PRIVKEY_FILE = "Private key file" + OCI_PRIVKEY_PASS = "Private key password" + OCI_TENANCY_OCID = "Tenancy OCID" + OCI_USER_OCID = "User OCID" + OCI_PUBKEY_FINGERPRINT = "Public key fingerprint" + OCI_REGION = "Region" OCI_COMPARTMENT_OCID = "Compartment OCID" - OCI_REGION = "Region (it can be empty if `OCI_AUTH_TYPE=instance_principal`)." - OCI_PRIVATE_KEY_PATH = "Private key file (ignored if `OCI_AUTH_TYPE=instance_principal`)" - OCI_PRIVATE_KEY_PASSWORD = "Private key password (ignored if `OCI_AUTH_TYPE=instance_principal`)" - OCI_TENANCY_OCID = "Tenancy OCID (ignored if `OCI_AUTH_TYPE=instance_principal`)" - OCI_USER_OCID = "User OCID (ignored if `OCI_AUTH_TYPE=instance_principal`)" - OCI_FINGERPRINT = "Public key fingerprint (ignored if `OCI_AUTH_TYPE=instance_principal`)" [Configuration.Additional] - OCI_AUTH_TYPE = "Authorization type. Possible values: 'instance_principal', '' (Default: '')" - TF_VAR_region = "Alias on `OCI_REGION`" - TF_VAR_fingerprint = "Alias on `OCI_FINGERPRINT`" - TF_VAR_user_ocid = "Alias on `OCI_USER_OCID`" - TF_VAR_tenancy_ocid = "Alias on `OCI_TENANCY_OCID`" - TF_VAR_private_key_path = "Alias on `OCI_PRIVATE_KEY_PATH`" - OCI_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - OCI_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - OCI_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - OCI_HTTP_TIMEOUT = "API request timeout in seconds (Default: 60)" + OCI_POLLING_INTERVAL = "Time between DNS propagation check" + OCI_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + OCI_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://docs.cloud.oracle.com/iaas/Content/DNS/Concepts/dnszonemanagement.htm" diff --git a/providers/dns/oraclecloud/oraclecloud_test.go b/providers/dns/oraclecloud/oraclecloud_test.go index 74ee06eac..9fff79ea1 100644 --- a/providers/dns/oraclecloud/oraclecloud_test.go +++ b/providers/dns/oraclecloud/oraclecloud_test.go @@ -6,31 +6,19 @@ import ( "crypto/x509" "encoding/base64" "encoding/pem" - "maps" - "net/http/httptest" "os" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/nrdcg/oci-go-sdk/common/v1065" + "github.com/oracle/oci-go-sdk/v65/common" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" -// Used by Instance Principal authentication. -const ( - envMetadataBaseURL = "OCI_METADATA_BASE_URL" - envSDKAuthClientRegionURL = "OCI_SDK_AUTH_CLIENT_REGION_URL" -) - var envTest = tester.NewEnvTest( envPrivKey, - EnvAuthType, - envMetadataBaseURL, - envSDKAuthClientRegionURL, EnvPrivKeyFile, EnvPrivKeyPass, EnvTenancyOCID, @@ -61,7 +49,7 @@ func TestNewDNSProvider(t *testing.T) { { desc: "success file", envVars: map[string]string{ - EnvPrivKeyFile: mustGeneratePrivateKeyFile(t, "secret1"), + EnvPrivKeyFile: mustGeneratePrivateKeyFile("secret1"), EnvPrivKeyPass: "secret1", EnvTenancyOCID: "ocid1.tenancy.oc1..secret", EnvUserOCID: "ocid1.user.oc1..secret", @@ -73,7 +61,7 @@ func TestNewDNSProvider(t *testing.T) { { desc: "missing credentials", envVars: map[string]string{}, - expected: "oraclecloud: some credentials information are missing: OCI_COMPARTMENT_OCID", + expected: "oraclecloud: some credentials information are missing: OCI_PRIVKEY,OCI_TENANCY_OCID,OCI_USER_OCID,OCI_PUBKEY_FINGERPRINT,OCI_REGION,OCI_COMPARTMENT_OCID", }, { desc: "missing CompartmentID", @@ -99,7 +87,7 @@ func TestNewDNSProvider(t *testing.T) { EnvRegion: "us-phoenix-1", EnvCompartmentOCID: "123", }, - expected: "oraclecloud: can not create client, bad configuration: no value provided for: OCI_PRIVKEY or OCI_PRIVATE_KEY or OCI_PRIVKEY_FILE or OCI_PRIVATE_KEY_PATH or TF_VAR_private_key_path", + expected: "oraclecloud: some credentials information are missing: OCI_PRIVKEY", }, { desc: "missing OCI_PRIVKEY_PASS", @@ -188,10 +176,8 @@ func TestNewDNSProvider(t *testing.T) { if privKeyFile != "" { _ = os.Remove(privKeyFile) } - envTest.RestoreEnv() }() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -211,74 +197,6 @@ func TestNewDNSProvider(t *testing.T) { } } -func TestNewDNSProvider_instance_principal(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAuthType: "instance_principal", - EnvCompartmentOCID: "123", - }, - }, - { - desc: "missing CompartmentID", - envVars: map[string]string{ - EnvAuthType: "instance_principal", - }, - expected: "oraclecloud: some credentials information are missing: OCI_COMPARTMENT_OCID", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer func() { - envTest.RestoreEnv() - }() - - envTest.ClearEnv() - - serverURL := servermock.NewBuilder( - func(server *httptest.Server) (string, error) { - return server.URL, nil - }). - Route("GET /instance/region", servermock.RawStringResponse("oc1")). - // To generate fake certificates: - // go run `go env GOROOT`/src/crypto/tls/generate_cert.go --host example.org --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h - Route("GET /identity/cert.pem", servermock.ResponseFromFixture("cert.pem")). - Route("GET /identity/key.pem", servermock.ResponseFromFixture("key.pem")). - Route("GET /identity/intermediate.pem", servermock.ResponseFromFixture("cert.pem")). - // https://github.com/oracle/oci-go-sdk/blob/413a2f277f95c5eb76e26a0e0833c396a518bf50/common/auth/jwt_test.go#L12 - Route("POST /v1/x509", servermock.RawStringResponse(`{"token":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImFzdyIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJvcGMub3JhY2xlLmNvbSIsImV4cCI6MTUxMTgzODc5MywiaWF0IjoxNTExODE3MTkzLCJpc3MiOiJhdXRoU2VydmljZS5vcmFjbGUuY29tIiwib3BjLWNlcnR0eXBlIjoiaW5zdGFuY2UiLCJvcGMtY29tcGFydG1lbnQiOiJvY2lkMS5jb21wYXJ0bWVudC5vYzEuLmJsdWhibHVoYmx1aCIsIm9wYy1pbnN0YW5jZSI6Im9jaWQxLmluc3RhbmNlLm9jMS5waHguYmx1aGJsdWhibHVoIiwib3BjLXRlbmFudCI6Im9jaWR2MTp0ZW5hbmN5Om9jMTpwaHg6MTIzNDU2Nzg5MDpibHVoYmx1aGJsdWgiLCJwdHlwZSI6Imluc3RhbmNlIiwic3ViIjoib2NpZDEuaW5zdGFuY2Uub2MxLnBoeC5ibHVoYmx1aGJsdWgiLCJ0ZW5hbnQiOiJvY2lkdjE6dGVuYW5jeTpvYzE6cGh4OjEyMzQ1Njc4OTA6Ymx1aGJsdWhibHVoIiwidHR5cGUiOiJ4NTA5In0.zen7q2yJSpMjzH4ym_H7VEwZA0-vTT4Wcild-HRfLxX6A1ej4tlpACa7A24j5JoZYI4mHooZVJ8e7ZezFenK0zZx5j8RbIjsqJKwroYXExOiBXLCUwMWOLXIndEsUzzGLqnPfKHXd80vrhMLmtkVTCJqBMzvPUSYkH_ciWgmjP9m0YETdQ9ifghkADhZGt9IlnOswg0s3Bx9ASwxFZEtom0BmU9GwEuITTTZfKvndk785BlNeZMOjhovaD97-LYpv5B_PiWEz8zialK5zxjijLCw06zyA8CQRQqmVCagNUPilfz_BcPyImzvFDuzQcPyDkTcsB7weX35tafHmA_Ul"}`)). - Build(t) - - envVars := map[string]string{ - envMetadataBaseURL: serverURL, - envSDKAuthClientRegionURL: serverURL, - } - - maps.Copy(envVars, test.envVars) - - envTest.Apply(envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.Error(t, err) - require.Contains(t, err.Error(), test.expected) - } - }) - } -} - func TestNewDNSProviderConfig(t *testing.T) { envTest.ClearEnv() defer envTest.RestoreEnv() @@ -333,7 +251,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -347,7 +264,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -357,20 +273,21 @@ func TestLiveCleanUp(t *testing.T) { require.NoError(t, err) } -func mockConfigurationProvider(keyPassphrase string) *environmentConfigurationProvider { +func mockConfigurationProvider(keyPassphrase string) *configProvider { envTest.Apply(map[string]string{ envPrivKey: mustGeneratePrivateKey("secret"), }) - return &environmentConfigurationProvider{ + return &configProvider{ values: map[string]string{ EnvCompartmentOCID: "test", - EnvPrivKeyPass: keyPassphrase, + EnvPrivKeyPass: "test", EnvTenancyOCID: "test", EnvUserOCID: "test", EnvPubKeyFingerprint: "test", EnvRegion: "test", }, + privateKeyPassphrase: keyPassphrase, } } @@ -383,27 +300,27 @@ func mustGeneratePrivateKey(pwd string) string { return base64.StdEncoding.EncodeToString(pem.EncodeToMemory(block)) } -func mustGeneratePrivateKeyFile(t *testing.T, pwd string) string { - t.Helper() - +func mustGeneratePrivateKeyFile(pwd string) string { block, err := generatePrivateKey(pwd) - require.NoError(t, err) + if err != nil { + panic(err) + } - file, err := os.CreateTemp(t.TempDir(), "lego_oci_*.pem") - require.NoError(t, err) - - defer func() { - _ = file.Close() - }() + file, err := os.CreateTemp("", "lego_oci_*.pem") + if err != nil { + panic(err) + } err = pem.Encode(file, block) - require.NoError(t, err) + if err != nil { + panic(err) + } return file.Name() } func generatePrivateKey(pwd string) (*pem.Block, error) { - key, err := rsa.GenerateKey(rand.Reader, 1024) + key, err := rsa.GenerateKey(rand.Reader, 512) if err != nil { return nil, err } diff --git a/providers/dns/otc/internal/client.go b/providers/dns/otc/internal/client.go index adb0682e1..59a685140 100644 --- a/providers/dns/otc/internal/client.go +++ b/providers/dns/otc/internal/client.go @@ -31,7 +31,7 @@ type Client struct { HTTPClient *http.Client } -func NewClient(username, password, domainName, projectName string) *Client { +func NewClient(username string, password string, domainName string, projectName string) *Client { return &Client{ username: username, password: password, @@ -42,8 +42,8 @@ func NewClient(username, password, domainName, projectName string) *Client { } } -func (c *Client) GetZoneID(ctx context.Context, zone string, privateZone bool) (string, error) { - zonesResp, err := c.getZones(ctx, zone, privateZone) +func (c *Client) GetZoneID(ctx context.Context, zone string) (string, error) { + zonesResp, err := c.getZones(ctx, zone) if err != nil { return "", err } @@ -62,18 +62,13 @@ func (c *Client) GetZoneID(ctx context.Context, zone string, privateZone bool) ( } // https://docs.otc.t-systems.com/domain-name-service/api-ref/apis/public_zone_management/querying_public_zones.html -func (c *Client) getZones(ctx context.Context, zone string, privateZone bool) (*ZonesResponse, error) { +func (c *Client) getZones(ctx context.Context, zone string) (*ZonesResponse, error) { c.muBaseURL.Lock() endpoint := c.baseURL.JoinPath("zones") c.muBaseURL.Unlock() query := endpoint.Query() query.Set("name", zone) - - if privateZone { - query.Set("type", "private") - } - endpoint.RawQuery = query.Encode() req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) @@ -82,7 +77,6 @@ func (c *Client) getZones(ctx context.Context, zone string, privateZone bool) (* } var zones ZonesResponse - err = c.do(req, &zones) if err != nil { return nil, err @@ -129,7 +123,6 @@ func (c *Client) getRecordSet(ctx context.Context, zoneID, fqdn string) (*Record } var recordSetsRes RecordSetsResponse - err = c.do(req, &recordSetsRes) if err != nil { return nil, err @@ -170,11 +163,9 @@ func (c *Client) DeleteRecordSet(ctx context.Context, zoneID, recordID string) e func (c *Client) do(req *http.Request, result any) error { c.muToken.Lock() - if c.token != "" { req.Header.Set("X-Auth-Token", c.token) } - c.muToken.Unlock() resp, err := c.HTTPClient.Do(req) @@ -205,7 +196,7 @@ func (c *Client) do(req *http.Request, result any) error { return nil } -func newJSONRequest[T string | *url.URL](ctx context.Context, method string, endpoint T, payload any) (*http.Request, error) { +func newJSONRequest[T string | *url.URL](ctx context.Context, method string, endpoint T, payload interface{}) (*http.Request, error) { buf := new(bytes.Buffer) if payload != nil { diff --git a/providers/dns/otc/internal/client_test.go b/providers/dns/otc/internal/client_test.go deleted file mode 100644 index 74b5bb3af..000000000 --- a/providers/dns/otc/internal/client_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package internal - -import ( - "context" - "net/http/httptest" - "net/url" - "strconv" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder( - func(server *httptest.Server) (*Client, error) { - client := NewClient("user", "secret", "example.com", "test") - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(), - ) -} - -func TestClient_GetZoneID(t *testing.T) { - client := mockBuilder(). - Route("GET /zones", - servermock.ResponseFromFixture("zones_GET.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "example.com.")). - Build(t) - - zoneID, err := client.GetZoneID(context.Background(), "example.com.", false) - require.NoError(t, err) - - assert.Equal(t, "123123", zoneID) -} - -func TestClient_GetZoneID_private(t *testing.T) { - client := mockBuilder(). - Route("GET /zones", - servermock.ResponseFromFixture("zones_GET.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "example.com."). - With("type", "private")). - Build(t) - - zoneID, err := client.GetZoneID(context.Background(), "example.com.", true) - require.NoError(t, err) - - assert.Equal(t, "123123", zoneID) -} - -func TestClient_GetZoneID_error(t *testing.T) { - client := mockBuilder(). - Route("GET /zones", - servermock.ResponseFromFixture("zones_GET_empty.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "example.com.")). - Build(t) - - _, err := client.GetZoneID(context.Background(), "example.com.", false) - require.EqualError(t, err, "zone example.com. not found") -} - -func TestClient_GetRecordSetID(t *testing.T) { - client := mockBuilder(). - Route("GET /zones/123123/recordsets", - servermock.ResponseFromFixture("zones-recordsets_GET.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "example.com."). - With("type", "TXT"), - ). - Build(t) - - recordSetID, err := client.GetRecordSetID(context.Background(), "123123", "example.com.") - require.NoError(t, err) - - assert.Equal(t, "321321", recordSetID) -} - -func TestClient_GetRecordSetID_error(t *testing.T) { - client := mockBuilder(). - Route("GET /zones/123123/recordsets", - servermock.ResponseFromFixture("zones-recordsets_GET_empty.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "example.com."). - With("type", "TXT"), - ). - Build(t) - - _, err := client.GetRecordSetID(context.Background(), "123123", "example.com.") - require.EqualError(t, err, "record not found") -} - -func TestClient_CreateRecordSet(t *testing.T) { - client := mockBuilder(). - Route("POST /zones/123123/recordsets", - servermock.ResponseFromFixture("zones-recordsets_POST.json"), - servermock.CheckRequestJSONBodyFromFixture("zones-recordsets_POST-request.json")). - Build(t) - - rs := RecordSets{ - Name: "_acme-challenge.example.com.", - Description: "Added TXT record for ACME dns-01 challenge using lego client", - Type: "TXT", - TTL: 300, - Records: []string{strconv.Quote("ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY")}, - } - err := client.CreateRecordSet(context.Background(), "123123", rs) - require.NoError(t, err) -} - -func TestClient_DeleteRecordSet(t *testing.T) { - client := mockBuilder(). - Route("DELETE /zones/123123/recordsets/321321", - servermock.ResponseFromFixture("zones-recordsets_DELETE.json")). - Build(t) - - err := client.DeleteRecordSet(context.Background(), "123123", "321321") - require.NoError(t, err) -} diff --git a/providers/dns/otc/internal/fixtures/zones-recordsets_POST-request.json b/providers/dns/otc/internal/fixtures/zones-recordsets_POST-request.json deleted file mode 100644 index 41cab72a8..000000000 --- a/providers/dns/otc/internal/fixtures/zones-recordsets_POST-request.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "_acme-challenge.example.com.", - "description": "Added TXT record for ACME dns-01 challenge using lego client", - "type": "TXT", - "ttl": 300, - "records": [ - "\"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"" - ] -} diff --git a/providers/dns/otc/internal/identity.go b/providers/dns/otc/internal/identity.go index 154ec65e2..f9e7cb08f 100644 --- a/providers/dns/otc/internal/identity.go +++ b/providers/dns/otc/internal/identity.go @@ -46,7 +46,6 @@ func (c *Client) Login(ctx context.Context) error { c.muToken.Lock() defer c.muToken.Unlock() - c.token = token if c.token == "" { @@ -97,7 +96,6 @@ func (c *Client) obtainUserToken(ctx context.Context, payload LoginRequest) (*To } var newToken TokenResponse - err = json.Unmarshal(raw, &newToken) if err != nil { return nil, "", errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) @@ -108,7 +106,6 @@ func (c *Client) obtainUserToken(ctx context.Context, payload LoginRequest) (*To func getBaseURL(tokenResp *TokenResponse) (*url.URL, error) { var endpoints []Endpoint - for _, v := range tokenResp.Token.Catalog { if v.Type == "dns" { endpoints = append(endpoints, v.Endpoints...) diff --git a/providers/dns/otc/internal/identity_test.go b/providers/dns/otc/internal/identity_test.go index 4dce72afc..18627869a 100644 --- a/providers/dns/otc/internal/identity_test.go +++ b/providers/dns/otc/internal/identity_test.go @@ -1,36 +1,25 @@ package internal import ( - "net/http/httptest" + "context" "net/url" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestClient_Login(t *testing.T) { - var serverURL *url.URL + mock := NewDNSServerMock(t) + mock.HandleAuthSuccessfully() - client := servermock.NewBuilder( - func(server *httptest.Server) (*Client, error) { - client := NewClient("user", "secret", "example.com", "test") - client.HTTPClient = server.Client() - client.IdentityEndpoint = server.URL + "/v3/auth/token" + client := NewClient("user", "secret", "example.com", "test") + client.IdentityEndpoint, _ = url.JoinPath(mock.GetServerURL(), "/v3/auth/token") - serverURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(), - ). - Route("POST /v3/auth/token", IdentityHandlerMock()). - Build(t) - - err := client.Login(t.Context()) + err := client.Login(context.Background()) require.NoError(t, err) + serverURL, _ := url.Parse(mock.GetServerURL()) assert.Equal(t, serverURL.JoinPath("v2").String(), client.baseURL.String()) assert.Equal(t, fakeOTCToken, client.token) } diff --git a/providers/dns/otc/internal/mock.go b/providers/dns/otc/internal/mock.go index 46da61e4c..2ed7f84de 100644 --- a/providers/dns/otc/internal/mock.go +++ b/providers/dns/otc/internal/mock.go @@ -2,13 +2,62 @@ package internal import ( "fmt" + "io" "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const fakeOTCToken = "62244bc21da68d03ebac94e6636ff01f" -func IdentityHandlerMock() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { +func writeFixture(rw http.ResponseWriter, filename string) { + file, err := os.Open(filepath.Join("internal", "fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, _ = io.Copy(rw, file) +} + +// DNSServerMock mock. +type DNSServerMock struct { + t *testing.T + server *httptest.Server + mux *http.ServeMux +} + +// NewDNSServerMock create a new DNSServerMock. +func NewDNSServerMock(t *testing.T) *DNSServerMock { + t.Helper() + + mux := http.NewServeMux() + + return &DNSServerMock{ + t: t, + server: httptest.NewServer(mux), + mux: mux, + } +} + +func (m *DNSServerMock) GetServerURL() string { + return m.server.URL +} + +// ShutdownServer creates the mock server. +func (m *DNSServerMock) ShutdownServer() { + m.server.Close() +} + +// HandleAuthSuccessfully Handle auth successfully. +func (m *DNSServerMock) HandleAuthSuccessfully() { + m.mux.HandleFunc("/v3/auth/token", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("X-Subject-Token", fakeOTCToken) _, _ = fmt.Fprintf(w, `{ @@ -20,7 +69,7 @@ func IdentityHandlerMock() http.HandlerFunc { "name": "", "endpoints": [ { - "url": "http://%s", + "url": "%s", "region": "eu-de", "region_id": "eu-de", "interface": "public", @@ -29,6 +78,87 @@ func IdentityHandlerMock() http.HandlerFunc { ] } ] - }}`, req.Context().Value(http.LocalAddrContextKey)) - } + }}`, m.server.URL) + }) +} + +// HandleListZonesSuccessfully Handle list zones successfully. +func (m *DNSServerMock) HandleListZonesSuccessfully() { + m.mux.HandleFunc("/v2/zones", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(m.t, http.MethodGet, r.Method) + assert.Equal(m.t, "/v2/zones", r.URL.Path) + assert.Equal(m.t, "name=example.com.", r.URL.RawQuery) + assert.Equal(m.t, "application/json", r.Header.Get("Accept")) + + writeFixture(w, "zones_GET.json") + }) +} + +// HandleListZonesEmpty Handle list zones empty. +func (m *DNSServerMock) HandleListZonesEmpty() { + m.mux.HandleFunc("/v2/zones", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(m.t, http.MethodGet, r.Method) + assert.Equal(m.t, "/v2/zones", r.URL.Path) + assert.Equal(m.t, "name=example.com.", r.URL.RawQuery) + assert.Equal(m.t, "application/json", r.Header.Get("Accept")) + + writeFixture(w, "zones_GET_empty.json") + }) +} + +// HandleDeleteRecordsetsSuccessfully Handle delete recordsets successfully. +func (m *DNSServerMock) HandleDeleteRecordsetsSuccessfully() { + m.mux.HandleFunc("/v2/zones/123123/recordsets/321321", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(m.t, http.MethodDelete, r.Method) + assert.Equal(m.t, "/v2/zones/123123/recordsets/321321", r.URL.Path) + assert.Equal(m.t, "application/json", r.Header.Get("Accept")) + + writeFixture(w, "zones-recordsets_DELETE.json") + }) +} + +// HandleListRecordsetsEmpty Handle list recordsets empty. +func (m *DNSServerMock) HandleListRecordsetsEmpty() { + m.mux.HandleFunc("/v2/zones/123123/recordsets", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(m.t, "/v2/zones/123123/recordsets", r.URL.Path) + assert.Equal(m.t, "name=_acme-challenge.example.com.&type=TXT", r.URL.RawQuery) + + writeFixture(w, "zones-recordsets_GET_empty.json") + }) +} + +// HandleListRecordsetsSuccessfully Handle list recordsets successfully. +func (m *DNSServerMock) HandleListRecordsetsSuccessfully() { + m.mux.HandleFunc("/v2/zones/123123/recordsets", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(m.t, "application/json", r.Header.Get("Accept")) + + if r.Method == http.MethodGet { + assert.Equal(m.t, "/v2/zones/123123/recordsets", r.URL.Path) + assert.Equal(m.t, "name=_acme-challenge.example.com.&type=TXT", r.URL.RawQuery) + + writeFixture(w, "zones-recordsets_GET.json") + return + } + + if r.Method == http.MethodPost { + assert.Equal(m.t, "application/json", r.Header.Get("Content-Type")) + + raw, err := io.ReadAll(r.Body) + require.NoError(m.t, err) + exceptedString := `{ + "name": "_acme-challenge.example.com.", + "description": "Added TXT record for ACME dns-01 challenge using lego client", + "type": "TXT", + "ttl": 300, + "records": ["\"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\""] + }` + + assert.JSONEq(m.t, exceptedString, string(raw)) + + writeFixture(w, "zones-recordsets_POST.json") + return + } + + http.Error(w, fmt.Sprintf("Expected method to be 'GET' or 'POST' but got '%s'", r.Method), http.StatusBadRequest) + }) } diff --git a/providers/dns/otc/internal/types.go b/providers/dns/otc/internal/types.go index e7bfe8fcb..38da4f110 100644 --- a/providers/dns/otc/internal/types.go +++ b/providers/dns/otc/internal/types.go @@ -41,8 +41,8 @@ type TokenResponse struct { } type Token struct { - User UserR `json:"user"` - Domain Domain `json:"domain"` + User UserR `json:"user,omitempty"` + Domain Domain `json:"domain,omitempty"` Catalog []Catalog `json:"catalog,omitempty"` Methods []string `json:"methods,omitempty"` Roles []Role `json:"roles,omitempty"` @@ -59,7 +59,7 @@ type Catalog struct { type UserR struct { ID string `json:"id,omitempty"` - Domain Domain `json:"domain"` + Domain Domain `json:"domain,omitempty"` Name string `json:"name,omitempty"` PasswordExpiresAt string `json:"password_expires_at,omitempty"` } @@ -106,7 +106,7 @@ type RecordSets struct { // ZonesResponse type ZonesResponse struct { - Links Links `json:"links"` + Links Links `json:"links,omitempty"` Zones []Zone `json:"zones"` Metadata Metadata `json:"metadata"` } diff --git a/providers/dns/otc/otc.go b/providers/dns/otc/otc.go index 65b362124..f2526b87e 100644 --- a/providers/dns/otc/otc.go +++ b/providers/dns/otc/otc.go @@ -5,16 +5,20 @@ import ( "context" "errors" "fmt" + "net" "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/otc/internal" ) +const defaultIdentityEndpoint = "https://iam.eu-de.otc.t-systems.com:443/v3/auth/tokens" + +// minTTL 300 is otc minimum value for TTL. +const minTTL = 300 + // Environment variables names. const ( envNamespace = "OTC_" @@ -24,7 +28,6 @@ const ( EnvPassword = envNamespace + "PASSWORD" EnvProjectName = envNamespace + "PROJECT_NAME" EnvIdentityEndpoint = envNamespace + "IDENTITY_ENDPOINT" - EnvPrivateZone = envNamespace + "PRIVATE_ZONE" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" @@ -33,22 +36,13 @@ const ( EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" ) -const defaultIdentityEndpoint = "https://iam.eu-de.otc.t-systems.com:443/v3/auth/tokens" - -// minTTL 300 is otc minimum value for TTL. -const minTTL = 300 - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { - DomainName string - ProjectName string - UserName string - Password string - IdentityEndpoint string - PrivateZone bool - + IdentityEndpoint string + DomainName string + ProjectName string + UserName string + Password string PropagationTimeout time.Duration PollingInterval time.Duration SequenceInterval time.Duration @@ -58,27 +52,28 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { - tr := &http.Transport{} - - defaultTransport, ok := http.DefaultTransport.(*http.Transport) - if ok { - tr = defaultTransport.Clone() - } - - // Workaround for keep alive bug in otc api - tr.DisableKeepAlives = true - return &Config{ - PrivateZone: env.GetOrDefaultBool(EnvPrivateZone, false), - IdentityEndpoint: env.GetOrDefaultString(EnvIdentityEndpoint, defaultIdentityEndpoint), - TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + IdentityEndpoint: env.GetOrDefaultString(EnvIdentityEndpoint, defaultIdentityEndpoint), SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), - Transport: tr, + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + + // Workaround for keep alive bug in otc api + DisableKeepAlives: true, + }, }, } } @@ -131,8 +126,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{config: config, client: client}, nil } @@ -152,7 +145,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("otc: %w", err) } - zoneID, err := d.client.GetZoneID(ctx, authZone, d.config.PrivateZone) + zoneID, err := d.client.GetZoneID(ctx, authZone) if err != nil { return fmt.Errorf("otc: unable to get zone: %w", err) } @@ -189,7 +182,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("otc: %w", err) } - zoneID, err := d.client.GetZoneID(ctx, authZone, d.config.PrivateZone) + zoneID, err := d.client.GetZoneID(ctx, authZone) if err != nil { return fmt.Errorf("otc: %w", err) } diff --git a/providers/dns/otc/otc.toml b/providers/dns/otc/otc.toml index e63077fda..e3c60158c 100644 --- a/providers/dns/otc/otc.toml +++ b/providers/dns/otc/otc.toml @@ -4,13 +4,7 @@ URL = "https://cloud.telekom.de/en" Code = "otc" Since = "v0.4.1" -Example = ''' -OTC_DOMAIN_NAME=domain_name \ -OTC_USER_NAME=user_name \ -OTC_PASSWORD=password \ -OTC_PROJECT_NAME=project_name \ -lego --dns otc -d '*.example.com' -d example.com run -''' +Example = '''''' [Configuration] [Configuration.Credentials] @@ -18,14 +12,13 @@ lego --dns otc -d '*.example.com' -d example.com run OTC_PASSWORD = "Password" OTC_PROJECT_NAME = "Project name" OTC_DOMAIN_NAME = "Domain name" + OTC_IDENTITY_ENDPOINT = "Identity endpoint URL" [Configuration.Additional] - OTC_IDENTITY_ENDPOINT = "Identity endpoint URL (default: https://iam.eu-de.otc.t-systems.com:443/v3/auth/tokens)" - OTC_PRIVATE_ZONE = "Set to true to use private zones only (default: use public zones only)" - OTC_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - OTC_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - OTC_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 60)" - OTC_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - OTC_HTTP_TIMEOUT = "API request timeout in seconds (Default: 10)" + OTC_POLLING_INTERVAL = "Time between DNS propagation check" + OTC_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + OTC_SEQUENCE_INTERVAL = "Time between sequential requests" + OTC_TTL = "The TTL of the TXT record used for the DNS challenge" + OTC_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://docs.otc.t-systems.com/domain-name-service/api-ref/index.html" diff --git a/providers/dns/otc/otc_test.go b/providers/dns/otc/otc_test.go index 518ce0f19..54907b69e 100644 --- a/providers/dns/otc/otc_test.go +++ b/providers/dns/otc/otc_test.go @@ -2,334 +2,129 @@ package otc import ( "fmt" - "net/http/httptest" + "os" "testing" - "time" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/go-acme/lego/v4/providers/dns/otc/internal" - "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" ) -const envDomain = envNamespace + "DOMAIN" +type OTCSuite struct { + suite.Suite -var envTest = tester.NewEnvTest( - EnvDomainName, - EnvUserName, - EnvPassword, - EnvPrivateZone, - EnvProjectName, - EnvIdentityEndpoint). - WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvDomainName: "example.com", - EnvUserName: "user", - EnvPassword: "secret", - EnvProjectName: "test", - }, - }, - { - desc: "missing credentials", - envVars: map[string]string{ - EnvDomainName: "", - EnvUserName: "", - EnvPassword: "", - EnvProjectName: "", - }, - expected: "otc: some credentials information are missing: OTC_DOMAIN_NAME,OTC_USER_NAME,OTC_PASSWORD,OTC_PROJECT_NAME", - }, - { - desc: "missing domain name", - envVars: map[string]string{ - EnvDomainName: "", - EnvUserName: "user", - EnvPassword: "secret", - EnvProjectName: "test", - }, - expected: "otc: some credentials information are missing: OTC_DOMAIN_NAME", - }, - { - desc: "missing username", - envVars: map[string]string{ - EnvDomainName: "example.com", - EnvUserName: "", - EnvPassword: "secret", - EnvProjectName: "test", - }, - expected: "otc: some credentials information are missing: OTC_USER_NAME", - }, - { - desc: "missing password", - envVars: map[string]string{ - EnvDomainName: "example.com", - EnvUserName: "user", - EnvPassword: "", - EnvProjectName: "test", - }, - expected: "otc: some credentials information are missing: OTC_PASSWORD", - }, - { - desc: "missing project name", - envVars: map[string]string{ - EnvDomainName: "example.com", - EnvUserName: "user", - EnvPassword: "secret", - EnvProjectName: "", - }, - expected: "otc: some credentials information are missing: OTC_PROJECT_NAME", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - } else { - require.EqualError(t, err, test.expected) - } - }) - } + mock *internal.DNSServerMock + envTest *tester.EnvTest } -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - domainName string - projectName string - username string - password string - expected string - }{ - { - desc: "success", - domainName: "example.com", - projectName: "test", - username: "user", - password: "secret", - }, - { - desc: "missing credentials", - expected: "otc: credentials missing", - }, - { - desc: "missing domain name", - domainName: "", - projectName: "test", - username: "user", - password: "secret", - expected: "otc: credentials missing", - }, - { - desc: "missing project name", - domainName: "example.com", - projectName: "", - username: "user", - password: "secret", - expected: "otc: credentials missing", - }, - { - desc: "missing username", - domainName: "example.com", - projectName: "test", - username: "", - password: "secret", - expected: "otc: credentials missing", - }, - { - desc: "missing password ", - domainName: "example.com", - projectName: "test", - username: "user", - password: "", - expected: "otc: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.DomainName = test.domainName - config.ProjectName = test.projectName - config.UserName = test.username - config.Password = test.password - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - } else { - require.EqualError(t, err, test.expected) - } - }) - } +func (s *OTCSuite) SetupTest() { + s.mock = internal.NewDNSServerMock(s.T()) + s.mock.HandleAuthSuccessfully() + s.envTest = tester.NewEnvTest( + EnvDomainName, + EnvUserName, + EnvPassword, + EnvProjectName, + EnvIdentityEndpoint, + ) } -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } +func (s *OTCSuite) TearDownTest() { + s.envTest.RestoreEnv() + s.mock.ShutdownServer() +} - envTest.RestoreEnv() +func TestTestSuite(t *testing.T) { + suite.Run(t, new(OTCSuite)) +} + +func (s *OTCSuite) createDNSProvider() (*DNSProvider, error) { + config := NewDefaultConfig() + config.UserName = "UserName" + config.Password = "Password" + config.DomainName = "DomainName" + config.ProjectName = "ProjectName" + config.IdentityEndpoint = fmt.Sprintf("%s/v3/auth/token", s.mock.GetServerURL()) + + return NewDNSProviderConfig(config) +} + +func (s *OTCSuite) TestLoginEnv() { + s.envTest.ClearEnv() + + s.envTest.Apply(map[string]string{ + EnvDomainName: "unittest1", + EnvUserName: "unittest2", + EnvPassword: "unittest3", + EnvProjectName: "unittest4", + EnvIdentityEndpoint: "unittest5", + }) provider, err := NewDNSProvider() - require.NoError(t, err) + s.Require().NoError(err) - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) + s.Equal("unittest1", provider.config.DomainName) + s.Equal("unittest2", provider.config.UserName) + s.Equal("unittest3", provider.config.Password) + s.Equal("unittest4", provider.config.ProjectName) + s.Equal("unittest5", provider.config.IdentityEndpoint) + + os.Setenv(EnvIdentityEndpoint, "") + + provider, err = NewDNSProvider() + s.Require().NoError(err) + + s.Equal("https://iam.eu-de.otc.t-systems.com:443/v3/auth/tokens", provider.config.IdentityEndpoint) } -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } +func (s *OTCSuite) TestLoginEnvEmpty() { + s.envTest.ClearEnv() - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - time.Sleep(1 * time.Second) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) + _, err := NewDNSProvider() + s.EqualError(err, "otc: some credentials information are missing: OTC_DOMAIN_NAME,OTC_USER_NAME,OTC_PASSWORD,OTC_PROJECT_NAME") } -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(false). - Route("GET /v2/zones", - servermock.ResponseFromInternal("zones_GET.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "example.com.")). - Route("POST /v2/zones/123123/recordsets", - servermock.Noop(), - servermock.CheckRequestJSONBodyFromInternal("zones-recordsets_POST-request.json")). - Build(t) +func (s *OTCSuite) TestDNSProvider_Present() { + s.mock.HandleListZonesSuccessfully() + s.mock.HandleListRecordsetsSuccessfully() - err := provider.Present("example.com", "", "123d==") - require.NoError(t, err) + provider, err := s.createDNSProvider() + s.Require().NoError(err) + + err = provider.Present("example.com", "", "foobar") + s.Require().NoError(err) } -func TestDNSProvider_Present_private(t *testing.T) { - provider := mockBuilder(true). - Route("GET /v2/zones", - servermock.ResponseFromInternal("zones_GET.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "example.com."). - With("type", "private")). - Route("POST /v2/zones/123123/recordsets", - servermock.Noop(), - servermock.CheckRequestJSONBodyFromInternal("zones-recordsets_POST-request.json")). - Build(t) +func (s *OTCSuite) TestDNSProvider_Present_EmptyZone() { + s.mock.HandleListZonesEmpty() + s.mock.HandleListRecordsetsSuccessfully() - err := provider.Present("example.com", "", "123d==") - require.NoError(t, err) + provider, err := s.createDNSProvider() + s.Require().NoError(err) + + err = provider.Present("example.com", "", "foobar") + s.Error(err) } -func TestDNSProvider_Present_emptyZone(t *testing.T) { - provider := mockBuilder(false). - Route("GET /v2/zones", - servermock.ResponseFromInternal("zones_GET_empty.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "example.com.")). - Build(t) +func (s *OTCSuite) TestDNSProvider_CleanUp() { + s.mock.HandleListZonesSuccessfully() + s.mock.HandleListRecordsetsSuccessfully() + s.mock.HandleDeleteRecordsetsSuccessfully() - err := provider.Present("example.com", "", "123d==") - require.EqualError(t, err, "otc: unable to get zone: zone example.com. not found") + provider, err := s.createDNSProvider() + s.Require().NoError(err) + + err = provider.CleanUp("example.com", "", "foobar") + s.Require().NoError(err) } -func TestDNSProvider_Cleanup(t *testing.T) { - provider := mockBuilder(false). - Route("GET /v2/zones", - servermock.ResponseFromInternal("zones_GET.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "example.com.")). - Route("GET /v2/zones/123123/recordsets", - servermock.ResponseFromInternal("zones-recordsets_GET.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "_acme-challenge.example.com."). - With("type", "TXT")). - Route("DELETE /v2/zones/123123/recordsets/321321", - servermock.ResponseFromInternal("zones-recordsets_DELETE.json")). - Build(t) +func (s *OTCSuite) TestDNSProvider_CleanUp_EmptyRecordset() { + s.mock.HandleListZonesSuccessfully() + s.mock.HandleListRecordsetsEmpty() - err := provider.CleanUp("example.com", "", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_Cleanup_private(t *testing.T) { - provider := mockBuilder(true). - Route("GET /v2/zones", - servermock.ResponseFromInternal("zones_GET.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "example.com."). - With("type", "private")). - Route("GET /v2/zones/123123/recordsets", - servermock.ResponseFromInternal("zones-recordsets_GET.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "_acme-challenge.example.com."). - With("type", "TXT")). - Route("DELETE /v2/zones/123123/recordsets/321321", - servermock.ResponseFromInternal("zones-recordsets_DELETE.json")). - Build(t) - - err := provider.CleanUp("example.com", "", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_Cleanup_emptyRecordset(t *testing.T) { - provider := mockBuilder(false). - Route("GET /v2/zones", - servermock.ResponseFromInternal("zones_GET.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "example.com.")). - Route("GET /v2/zones/123123/recordsets", - servermock.ResponseFromInternal("zones-recordsets_GET_empty.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "_acme-challenge.example.com."). - With("type", "TXT")). - Build(t) - - err := provider.CleanUp("example.com", "", "123d==") - require.EqualError(t, err, "otc: unable to get record _acme-challenge.example.com. for zone example.com: record not found") -} - -func mockBuilder(private bool) *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.HTTPClient = server.Client() - config.UserName = "user" - config.Password = "secret" - config.DomainName = "example.com" - config.ProjectName = "test" - config.IdentityEndpoint = fmt.Sprintf("%s/v3/auth/token", server.URL) - config.PrivateZone = private - - return NewDNSProviderConfig(config) - }, - servermock.CheckHeader().WithJSONHeaders(), - ). - Route("POST /v3/auth/token", internal.IdentityHandlerMock()) + provider, err := s.createDNSProvider() + s.Require().NoError(err) + + err = provider.CleanUp("example.com", "", "foobar") + s.Require().Error(err) } diff --git a/providers/dns/ovh/ovh.go b/providers/dns/ovh/ovh.go index a8d12d819..edc3e6e30 100644 --- a/providers/dns/ovh/ovh.go +++ b/providers/dns/ovh/ovh.go @@ -8,17 +8,14 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" "github.com/ovh/go-ovh/ovh" ) // OVH API reference: https://eu.api.ovh.com/ // Create a Token: https://eu.api.ovh.com/createToken/ -// Create a OAuth2 client: https://eu.api.ovh.com/console/?section=%2Fme&branch=v1#post-/me/api/oauth2/client +// Create a OAuth2 client: https://eu.api.ovh.com/console-preview/?section=%2Fme&branch=v1#post-/me/api/oauth2/client // Environment variables names. const ( @@ -45,11 +42,6 @@ const ( EnvClientSecret = envNamespace + "CLIENT_SECRET" ) -// EnvAccessToken Authenticate using Access Token client. -const EnvAccessToken = envNamespace + "ACCESS_TOKEN" - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Record a DNS record. type Record struct { ID int64 `json:"id,omitempty"` @@ -76,14 +68,16 @@ type Config struct { OAuth2Config *OAuth2Config - AccessToken string - PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } +func (c *Config) hasAppKeyAuth() bool { + return c.ApplicationKey != "" || c.ApplicationSecret != "" || c.ConsumerKey != "" +} + // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ @@ -96,15 +90,10 @@ func NewDefaultConfig() *Config { } } -func (c *Config) hasAppKeyAuth() bool { - return c.ApplicationKey != "" || c.ApplicationSecret != "" || c.ConsumerKey != "" -} - // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - config *Config - client *ovh.Client - + config *Config + client *ovh.Client recordIDs map[string]int64 recordIDsMu sync.Mutex } @@ -113,25 +102,9 @@ type DNSProvider struct { // Credentials must be passed in the environment variables: // OVH_ENDPOINT (must be either "ovh-eu" or "ovh-ca"), OVH_APPLICATION_KEY, OVH_APPLICATION_SECRET, OVH_CONSUMER_KEY. func NewDNSProvider() (*DNSProvider, error) { - config := NewDefaultConfig() - - // https://github.com/ovh/go-ovh/blob/6817886d12a8c5650794b28da635af9fcdfd1162/ovh/configuration.go#L105 - config.APIEndpoint = env.GetOrDefaultString(EnvEndpoint, "ovh-eu") - - config.ApplicationKey = env.GetOrFile(EnvApplicationKey) - config.ApplicationSecret = env.GetOrFile(EnvApplicationSecret) - config.ConsumerKey = env.GetOrFile(EnvConsumerKey) - - config.AccessToken = env.GetOrFile(EnvAccessToken) - - clientID := env.GetOrFile(EnvClientID) - clientSecret := env.GetOrFile(EnvClientSecret) - - if clientID != "" || clientSecret != "" { - config.OAuth2Config = &OAuth2Config{ - ClientID: clientID, - ClientSecret: clientSecret, - } + config, err := createConfigFromEnvVars() + if err != nil { + return nil, fmt.Errorf("ovh: %w", err) } return NewDNSProviderConfig(config) @@ -143,20 +116,8 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("ovh: the configuration of the DNS provider is nil") } - if config.OAuth2Config != nil && config.hasAppKeyAuth() && config.AccessToken != "" { - return nil, errors.New("ovh: can't use multiple authentication systems (ApplicationKey, OAuth2, Access Token)") - } - - if config.OAuth2Config != nil && config.AccessToken != "" { - return nil, errors.New("ovh: can't use multiple authentication systems (OAuth2, Access Token)") - } - if config.OAuth2Config != nil && config.hasAppKeyAuth() { - return nil, errors.New("ovh: can't use multiple authentication systems (ApplicationKey, OAuth2)") - } - - if config.hasAppKeyAuth() && config.AccessToken != "" { - return nil, errors.New("ovh: can't use multiple authentication systems (ApplicationKey, Access Token)") + return nil, errors.New("ovh: can't use both authentication systems (ApplicationKey and OAuth2)") } client, err := newClient(config) @@ -164,6 +125,8 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, fmt.Errorf("ovh: %w", err) } + client.Client = config.HTTPClient + return &DNSProvider{ config: config, client: client, @@ -175,6 +138,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) + // Parse domain name authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { return fmt.Errorf("ovh: could not find zone for domain %q: %w", domain, err) @@ -192,7 +156,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // Create TXT record var respData Record - err = d.client.Post(reqURL, reqData, &respData) if err != nil { return fmt.Errorf("ovh: error when call api to add record (%s): %w", reqURL, err) @@ -200,7 +163,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // Apply the change reqURL = fmt.Sprintf("/domain/zone/%s/refresh", authZone) - err = d.client.Post(reqURL, nil, nil) if err != nil { return fmt.Errorf("ovh: error when call api to refresh zone (%s): %w", reqURL, err) @@ -221,7 +183,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() - if !ok { return fmt.Errorf("ovh: unknown record ID for '%s'", info.EffectiveFQDN) } @@ -242,7 +203,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { // Apply the change reqURL = fmt.Sprintf("/domain/zone/%s/refresh", authZone) - err = d.client.Post(reqURL, nil, nil) if err != nil { return fmt.Errorf("ovh: error when call api to refresh zone (%s): %w", reqURL, err) @@ -262,34 +222,94 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } -func newClient(config *Config) (*ovh.Client, error) { - var ( - client *ovh.Client - err error - ) +func createConfigFromEnvVars() (*Config, error) { + firstAppKeyEnvVar := findFirstValuedEnvVar(EnvApplicationKey, EnvApplicationSecret, EnvConsumerKey) + firstOAuth2EnvVar := findFirstValuedEnvVar(EnvClientID, EnvClientSecret) - switch { - case config.hasAppKeyAuth(): - client, err = ovh.NewClient(config.APIEndpoint, config.ApplicationKey, config.ApplicationSecret, config.ConsumerKey) - case config.OAuth2Config != nil: - client, err = ovh.NewOAuth2Client(config.APIEndpoint, config.OAuth2Config.ClientID, config.OAuth2Config.ClientSecret) - case config.AccessToken != "": - client, err = ovh.NewAccessTokenClient(config.APIEndpoint, config.AccessToken) - default: - client, err = ovh.NewDefaultClient() + if firstAppKeyEnvVar != "" && firstOAuth2EnvVar != "" { + return nil, fmt.Errorf("can't use both %s and %s at the same time", firstAppKeyEnvVar, firstOAuth2EnvVar) } + config := NewDefaultConfig() + + if firstOAuth2EnvVar != "" { + values, err := env.Get(EnvEndpoint, EnvClientID, EnvClientSecret) + if err != nil { + return nil, err + } + + config.APIEndpoint = values[EnvEndpoint] + config.OAuth2Config = &OAuth2Config{ + ClientID: values[EnvClientID], + ClientSecret: values[EnvClientSecret], + } + + return config, nil + } + + values, err := env.Get(EnvEndpoint, EnvApplicationKey, EnvApplicationSecret, EnvConsumerKey) + if err != nil { + return nil, err + } + + config.APIEndpoint = values[EnvEndpoint] + + config.ApplicationKey = values[EnvApplicationKey] + config.ApplicationSecret = values[EnvApplicationSecret] + config.ConsumerKey = values[EnvConsumerKey] + + return config, nil +} + +func findFirstValuedEnvVar(envVars ...string) string { + for _, envVar := range envVars { + if env.GetOrFile(envVar) != "" { + return envVar + } + } + + return "" +} + +func newClient(config *Config) (*ovh.Client, error) { + if config.OAuth2Config == nil { + return newClientApplicationKey(config) + } + + return newClientOAuth2(config) +} + +func newClientApplicationKey(config *Config) (*ovh.Client, error) { + if config.APIEndpoint == "" || config.ApplicationKey == "" || config.ApplicationSecret == "" || config.ConsumerKey == "" { + return nil, errors.New("credentials are missing") + } + + client, err := ovh.NewClient( + config.APIEndpoint, + config.ApplicationKey, + config.ApplicationSecret, + config.ConsumerKey, + ) if err != nil { return nil, fmt.Errorf("new client: %w", err) } - client.UserAgent = useragent.Get() + return client, nil +} - if config.HTTPClient != nil { - client.Client = config.HTTPClient +func newClientOAuth2(config *Config) (*ovh.Client, error) { + if config.APIEndpoint == "" || config.OAuth2Config.ClientID == "" || config.OAuth2Config.ClientSecret == "" { + return nil, errors.New("credentials are missing") } - client.Client = clientdebug.Wrap(client.Client) + client, err := ovh.NewOAuth2Client( + config.APIEndpoint, + config.OAuth2Config.ClientID, + config.OAuth2Config.ClientSecret, + ) + if err != nil { + return nil, fmt.Errorf("new OAuth2 client: %w", err) + } return client, nil } diff --git a/providers/dns/ovh/ovh.toml b/providers/dns/ovh/ovh.toml index abf22bd7a..1597d280d 100644 --- a/providers/dns/ovh/ovh.toml +++ b/providers/dns/ovh/ovh.toml @@ -11,20 +11,14 @@ OVH_APPLICATION_KEY=1234567898765432 \ OVH_APPLICATION_SECRET=b9841238feb177a84330febba8a832089 \ OVH_CONSUMER_KEY=256vfsd347245sdfg \ OVH_ENDPOINT=ovh-eu \ -lego --dns ovh -d '*.example.com' -d example.com run - -# Or Access Token: - -OVH_ACCESS_TOKEN=xxx \ -OVH_ENDPOINT=ovh-eu \ -lego --dns ovh -d '*.example.com' -d example.com run +lego --email you@example.com --dns ovh --domains my.example.org run # Or OAuth2: OVH_CLIENT_ID=yyy \ OVH_CLIENT_SECRET=xxx \ OVH_ENDPOINT=ovh-eu \ -lego --dns ovh -d '*.example.com' -d example.com run +lego --email you@example.com --dns ovh --domains my.example.org run ''' Additional = ''' @@ -74,12 +68,11 @@ Both authentication methods cannot be used at the same time. OVH_CONSUMER_KEY = "Consumer key (Application Key authentication)" OVH_CLIENT_ID = "Client ID (OAuth2)" OVH_CLIENT_SECRET = "Client secret (OAuth2)" - OVH_ACCESS_TOKEN = "Access token" [Configuration.Additional] - OVH_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - OVH_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - OVH_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - OVH_HTTP_TIMEOUT = "API request timeout in seconds (Default: 180)" + OVH_POLLING_INTERVAL = "Time between DNS propagation check" + OVH_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + OVH_TTL = "The TTL of the TXT record used for the DNS challenge" + OVH_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://eu.api.ovh.com/" diff --git a/providers/dns/ovh/ovh_test.go b/providers/dns/ovh/ovh_test.go index 332e7f192..cac88e90b 100644 --- a/providers/dns/ovh/ovh_test.go +++ b/providers/dns/ovh/ovh_test.go @@ -16,8 +16,7 @@ var envTest = tester.NewEnvTest( EnvApplicationSecret, EnvConsumerKey, EnvClientID, - EnvClientSecret, - EnvAccessToken). + EnvClientSecret). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { @@ -35,6 +34,16 @@ func TestNewDNSProvider(t *testing.T) { EnvConsumerKey: "D", }, }, + { + desc: "application key: missing endpoint", + envVars: map[string]string{ + EnvEndpoint: "", + EnvApplicationKey: "B", + EnvApplicationSecret: "C", + EnvConsumerKey: "D", + }, + expected: "ovh: some credentials information are missing: OVH_ENDPOINT", + }, { desc: "application key: missing invalid endpoint", envVars: map[string]string{ @@ -53,7 +62,7 @@ func TestNewDNSProvider(t *testing.T) { EnvApplicationSecret: "C", EnvConsumerKey: "D", }, - expected: "ovh: new client: invalid authentication config, both application_key and application_secret must be given", + expected: "ovh: some credentials information are missing: OVH_APPLICATION_KEY", }, { desc: "application key: missing application secret", @@ -63,7 +72,17 @@ func TestNewDNSProvider(t *testing.T) { EnvApplicationSecret: "", EnvConsumerKey: "D", }, - expected: "ovh: new client: invalid authentication config, both application_key and application_secret must be given", + expected: "ovh: some credentials information are missing: OVH_APPLICATION_SECRET", + }, + { + desc: "application key: missing consumer key", + envVars: map[string]string{ + EnvEndpoint: "ovh-eu", + EnvApplicationKey: "B", + EnvApplicationSecret: "C", + EnvConsumerKey: "", + }, + expected: "ovh: some credentials information are missing: OVH_CONSUMER_KEY", }, { desc: "oauth2: success", @@ -73,13 +92,6 @@ func TestNewDNSProvider(t *testing.T) { EnvClientSecret: "F", }, }, - { - desc: "access token: success", - envVars: map[string]string{ - EnvEndpoint: "ovh-eu", - EnvAccessToken: "G", - }, - }, { desc: "oauth2: missing client secret", envVars: map[string]string{ @@ -87,7 +99,7 @@ func TestNewDNSProvider(t *testing.T) { EnvClientID: "E", EnvClientSecret: "", }, - expected: "ovh: new client: invalid oauth2 config, both client_id and client_secret must be given", + expected: "ovh: some credentials information are missing: OVH_CLIENT_SECRET", }, { desc: "oauth2: missing client ID", @@ -96,7 +108,7 @@ func TestNewDNSProvider(t *testing.T) { EnvClientID: "", EnvClientSecret: "F", }, - expected: "ovh: new client: invalid oauth2 config, both client_id and client_secret must be given", + expected: "ovh: some credentials information are missing: OVH_CLIENT_ID", }, { desc: "missing credentials", @@ -107,25 +119,11 @@ func TestNewDNSProvider(t *testing.T) { EnvConsumerKey: "", EnvClientID: "", EnvClientSecret: "", - EnvAccessToken: "", }, - expected: "ovh: new client: missing authentication information, you need to provide one of the following: application_key/application_secret, client_id/client_secret, or access_token", + expected: "ovh: some credentials information are missing: OVH_ENDPOINT,OVH_APPLICATION_KEY,OVH_APPLICATION_SECRET,OVH_CONSUMER_KEY", }, { - desc: "mixed auth (all)", - envVars: map[string]string{ - EnvEndpoint: "ovh-eu", - EnvApplicationKey: "B", - EnvApplicationSecret: "C", - EnvConsumerKey: "D", - EnvClientID: "E", - EnvClientSecret: "F", - EnvAccessToken: "G", - }, - expected: "ovh: can't use multiple authentication systems (ApplicationKey, OAuth2, Access Token)", - }, - { - desc: "mixed auth (ApplicationKey, OAuth2)", + desc: "mixed auth", envVars: map[string]string{ EnvEndpoint: "ovh-eu", EnvApplicationKey: "B", @@ -134,35 +132,13 @@ func TestNewDNSProvider(t *testing.T) { EnvClientID: "E", EnvClientSecret: "F", }, - expected: "ovh: can't use multiple authentication systems (ApplicationKey, OAuth2)", - }, - { - desc: "mixed auth (ApplicationKey, Access Token)", - envVars: map[string]string{ - EnvEndpoint: "ovh-eu", - EnvApplicationKey: "B", - EnvApplicationSecret: "C", - EnvConsumerKey: "D", - EnvAccessToken: "G", - }, - expected: "ovh: can't use multiple authentication systems (ApplicationKey, Access Token)", - }, - { - desc: "mixed auth (OAuth2, Access Token)", - envVars: map[string]string{ - EnvEndpoint: "ovh-eu", - EnvClientID: "E", - EnvClientSecret: "F", - EnvAccessToken: "G", - }, - expected: "ovh: can't use multiple authentication systems (OAuth2, Access Token)", + expected: "ovh: can't use both OVH_APPLICATION_KEY and OVH_CLIENT_ID at the same time", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -191,7 +167,6 @@ func TestNewDNSProviderConfig(t *testing.T) { consumerKey string clientID string clientSecret string - accessToken string expected string }{ { @@ -202,11 +177,12 @@ func TestNewDNSProviderConfig(t *testing.T) { consumerKey: "D", }, { - desc: "application key: default api endpoint", + desc: "application key: missing api endpoint", apiEndpoint: "", applicationKey: "B", applicationSecret: "C", consumerKey: "D", + expected: "ovh: credentials are missing", }, { desc: "application key: invalid api endpoint", @@ -222,7 +198,7 @@ func TestNewDNSProviderConfig(t *testing.T) { applicationKey: "", applicationSecret: "C", consumerKey: "D", - expected: "ovh: new client: invalid authentication config, both application_key and application_secret must be given", + expected: "ovh: credentials are missing", }, { desc: "application key: missing application secret", @@ -230,7 +206,15 @@ func TestNewDNSProviderConfig(t *testing.T) { applicationKey: "B", applicationSecret: "", consumerKey: "D", - expected: "ovh: new client: invalid authentication config, both application_key and application_secret must be given", + expected: "ovh: credentials are missing", + }, + { + desc: "application key: missing consumer key", + apiEndpoint: "ovh-eu", + applicationKey: "B", + applicationSecret: "C", + consumerKey: "", + expected: "ovh: credentials are missing", }, { desc: "oauth2: success", @@ -239,84 +223,51 @@ func TestNewDNSProviderConfig(t *testing.T) { clientSecret: "C", }, { - desc: "oauth2: default api endpoint", + desc: "oauth2: missing api endpoint", apiEndpoint: "", clientID: "B", clientSecret: "C", + expected: "ovh: credentials are missing", }, { desc: "oauth2: invalid api endpoint", apiEndpoint: "foobar", clientID: "B", clientSecret: "C", - expected: "ovh: new client: unknown endpoint 'foobar', consider checking 'Endpoints' list or using an URL", + expected: "ovh: new OAuth2 client: unknown endpoint 'foobar', consider checking 'Endpoints' list or using an URL", }, { desc: "oauth2: missing client id", apiEndpoint: "ovh-eu", clientID: "", clientSecret: "C", - expected: "ovh: new client: invalid oauth2 config, both client_id and client_secret must be given", + expected: "ovh: credentials are missing", }, { desc: "oauth2: missing client secret", apiEndpoint: "ovh-eu", clientID: "B", clientSecret: "", - expected: "ovh: new client: invalid oauth2 config, both client_id and client_secret must be given", - }, - { - desc: "access token: success", - apiEndpoint: "ovh-eu", - accessToken: "G", + expected: "ovh: credentials are missing", }, { desc: "missing credentials", - expected: "ovh: new client: missing authentication information, you need to provide one of the following: application_key/application_secret, client_id/client_secret, or access_token", + expected: "ovh: credentials are missing", }, { - desc: "mixed auth (all)", + desc: "mixed auth", apiEndpoint: "ovh-eu", applicationKey: "B", applicationSecret: "C", consumerKey: "D", clientID: "B", clientSecret: "C", - accessToken: "G", - expected: "ovh: can't use multiple authentication systems (ApplicationKey, OAuth2, Access Token)", - }, - { - desc: "mixed auth (ApplicationKey, OAuth2)", - apiEndpoint: "ovh-eu", - applicationKey: "B", - applicationSecret: "C", - consumerKey: "D", - clientID: "B", - clientSecret: "C", - expected: "ovh: can't use multiple authentication systems (ApplicationKey, OAuth2)", - }, - { - desc: "mixed auth (ApplicationKey, Access Token)", - apiEndpoint: "ovh-eu", - applicationKey: "B", - applicationSecret: "C", - consumerKey: "D", - accessToken: "G", - expected: "ovh: can't use multiple authentication systems (ApplicationKey, Access Token)", - }, - { - desc: "mixed auth (OAuth2, Access Token)", - apiEndpoint: "ovh-eu", - clientID: "B", - clientSecret: "C", - accessToken: "G", - expected: "ovh: can't use multiple authentication systems (OAuth2, Access Token)", + expected: "ovh: can't use both authentication systems (ApplicationKey and OAuth2)", }, } // The OVH client use the same env vars than lego, so it requires to clean them. defer envTest.RestoreEnv() - envTest.ClearEnv() for _, test := range testCases { @@ -326,7 +277,6 @@ func TestNewDNSProviderConfig(t *testing.T) { config.ApplicationKey = test.applicationKey config.ApplicationSecret = test.applicationSecret config.ConsumerKey = test.consumerKey - config.AccessToken = test.accessToken if test.clientID != "" || test.clientSecret != "" { config.OAuth2Config = &OAuth2Config{ @@ -356,7 +306,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -370,7 +319,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/pdns/internal/client.go b/providers/dns/pdns/internal/client.go index f72dd4d78..bc525c578 100644 --- a/providers/dns/pdns/internal/client.go +++ b/providers/dns/pdns/internal/client.go @@ -18,9 +18,6 @@ import ( "github.com/miekg/dns" ) -// APIKeyHeader API key header. -const APIKeyHeader = "X-Api-Key" - // Client the PowerDNS API client. type Client struct { serverName string @@ -69,7 +66,6 @@ func (c *Client) getAPIVersion(ctx context.Context) (int, error) { } var versions []apiVersion - err = json.Unmarshal(result, &versions) if err != nil { return 0, err @@ -99,7 +95,6 @@ func (c *Client) GetHostedZone(ctx context.Context, authZone string) (*HostedZon } var zone HostedZone - err = json.Unmarshal(result, &zone) if err != nil { return nil, err @@ -168,7 +163,7 @@ func (c *Client) joinPath(elem ...string) *url.URL { } func (c *Client) do(req *http.Request) (json.RawMessage, error) { - req.Header.Set(APIKeyHeader, c.apiKey) + req.Header.Set("X-API-Key", c.apiKey) resp, err := c.HTTPClient.Do(req) if err != nil { @@ -182,7 +177,6 @@ func (c *Client) do(req *http.Request) (json.RawMessage, error) { } var msg json.RawMessage - err = json.NewDecoder(resp.Body).Decode(&msg) if err != nil { if errors.Is(err, io.EOF) { @@ -196,12 +190,10 @@ func (c *Client) do(req *http.Request) (json.RawMessage, error) { // check for PowerDNS error message if len(msg) > 0 && msg[0] == '{' { var errInfo apiError - err = json.Unmarshal(msg, &errInfo) if err != nil { return nil, errutils.NewUnmarshalError(req, resp.StatusCode, msg, err) } - if errInfo.ShortMsg != "" { return nil, fmt.Errorf("error talking to PDNS API: %w", errInfo) } diff --git a/providers/dns/pdns/internal/client_test.go b/providers/dns/pdns/internal/client_test.go index 17f05095f..b0eb9d2ed 100644 --- a/providers/dns/pdns/internal/client_test.go +++ b/providers/dns/pdns/internal/client_test.go @@ -1,27 +1,66 @@ package internal import ( + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - serverURL, _ := url.Parse(server.URL) +func setupTest(t *testing.T, method, pattern string, status int, file string) *Client { + t.Helper() - client := NewClient(serverURL, "server", 0, "secret") - client.HTTPClient = server.Client() + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders().With(APIKeyHeader, "secret")) + mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest) + return + } + + apiKey := req.Header.Get("X-API-Key") + if apiKey != "secret" { + http.Error(rw, fmt.Sprintf("invalid credentials: %s", apiKey), http.StatusBadRequest) + return + } + + if file == "" { + rw.WriteHeader(status) + return + } + + open, err := os.Open(filepath.Join("fixtures", file)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = open.Close() }() + + rw.WriteHeader(status) + _, err = io.Copy(rw, open) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + serverURL, _ := url.Parse(server.URL) + + client := NewClient(serverURL, "server", 0, "secret") + client.HTTPClient = server.Client() + + return client } func TestClient_joinPath(t *testing.T) { @@ -121,14 +160,10 @@ func TestClient_joinPath(t *testing.T) { } func TestClient_GetHostedZone(t *testing.T) { - client := mockBuilder(). - Route("GET /api/v1/servers/server/zones/example.org.", - servermock.ResponseFromFixture("zone.json")). - Build(t) - + client := setupTest(t, http.MethodGet, "/api/v1/servers/server/zones/example.org.", http.StatusOK, "zone.json") client.apiVersion = 1 - zone, err := client.GetHostedZone(t.Context(), "example.org.") + zone, err := client.GetHostedZone(context.Background(), "example.org.") require.NoError(t, err) expected := &HostedZone{ @@ -168,27 +203,18 @@ func TestClient_GetHostedZone(t *testing.T) { } func TestClient_GetHostedZone_error(t *testing.T) { - client := mockBuilder(). - Route("GET /api/v1/servers/server/zones/example.org.", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnprocessableEntity)). - Build(t) - + client := setupTest(t, http.MethodGet, "/api/v1/servers/server/zones/example.org.", http.StatusUnprocessableEntity, "error.json") client.apiVersion = 1 - _, err := client.GetHostedZone(t.Context(), "example.org.") + _, err := client.GetHostedZone(context.Background(), "example.org.") require.ErrorAs(t, err, &apiError{}) } func TestClient_GetHostedZone_v0(t *testing.T) { - client := mockBuilder(). - Route("GET /servers/server/zones/example.org.", - servermock.ResponseFromFixture("zone.json")). - Build(t) - + client := setupTest(t, http.MethodGet, "/servers/server/zones/example.org.", http.StatusOK, "zone.json") client.apiVersion = 0 - zone, err := client.GetHostedZone(t.Context(), "example.org.") + zone, err := client.GetHostedZone(context.Background(), "example.org.") require.NoError(t, err) expected := &HostedZone{ @@ -228,12 +254,7 @@ func TestClient_GetHostedZone_v0(t *testing.T) { } func TestClient_UpdateRecords(t *testing.T) { - client := mockBuilder(). - Route("PATCH /api/v1/servers/localhost/zones/example.org.", - servermock.ResponseFromFixture("zone.json"), - servermock.CheckRequestJSONBodyFromFixture("zone-request.json")). - Build(t) - + client := setupTest(t, http.MethodPatch, "/api/v1/servers/localhost/zones/example.org.", http.StatusOK, "zone.json") client.apiVersion = 1 client.serverName = "localhost" @@ -258,17 +279,12 @@ func TestClient_UpdateRecords(t *testing.T) { }}, } - err := client.UpdateRecords(t.Context(), zone, rrSets) + err := client.UpdateRecords(context.Background(), zone, rrSets) require.NoError(t, err) } func TestClient_UpdateRecords_NonRootApi(t *testing.T) { - client := mockBuilder(). - Route("PATCH /some/path/api/v1/servers/localhost/zones/example.org.", - servermock.ResponseFromFixture("zone.json"), - servermock.CheckRequestJSONBodyFromFixture("zone-request.json")). - Build(t) - + client := setupTest(t, http.MethodPatch, "/some/path/api/v1/servers/localhost/zones/example.org.", http.StatusOK, "zone.json") client.Host = client.Host.JoinPath("some", "path") client.apiVersion = 1 client.serverName = "localhost" @@ -294,17 +310,12 @@ func TestClient_UpdateRecords_NonRootApi(t *testing.T) { }}, } - err := client.UpdateRecords(t.Context(), zone, rrSets) + err := client.UpdateRecords(context.Background(), zone, rrSets) require.NoError(t, err) } func TestClient_UpdateRecords_v0(t *testing.T) { - client := mockBuilder(). - Route("PATCH /servers/localhost/zones/example.org.", - servermock.ResponseFromFixture("zone.json"), - servermock.CheckRequestJSONBodyFromFixture("zone-request.json")). - Build(t) - + client := setupTest(t, http.MethodPatch, "/servers/localhost/zones/example.org.", http.StatusOK, "zone.json") client.apiVersion = 0 client.serverName = "localhost" @@ -329,15 +340,12 @@ func TestClient_UpdateRecords_v0(t *testing.T) { }}, } - err := client.UpdateRecords(t.Context(), zone, rrSets) + err := client.UpdateRecords(context.Background(), zone, rrSets) require.NoError(t, err) } func TestClient_Notify(t *testing.T) { - client := mockBuilder(). - Route("PUT /api/v1/servers/localhost/zones/example.org./notify", nil). - Build(t) - + client := setupTest(t, http.MethodPut, "/api/v1/servers/localhost/zones/example.org./notify", http.StatusOK, "") client.apiVersion = 1 client.serverName = "localhost" @@ -348,15 +356,12 @@ func TestClient_Notify(t *testing.T) { Kind: "Master", } - err := client.Notify(t.Context(), zone) + err := client.Notify(context.Background(), zone) require.NoError(t, err) } func TestClient_Notify_NonRootApi(t *testing.T) { - client := mockBuilder(). - Route("PUT /some/path/api/v1/servers/localhost/zones/example.org./notify", nil). - Build(t) - + client := setupTest(t, http.MethodPut, "/some/path/api/v1/servers/localhost/zones/example.org./notify", http.StatusOK, "") client.Host = client.Host.JoinPath("some", "path") client.apiVersion = 1 client.serverName = "localhost" @@ -368,15 +373,12 @@ func TestClient_Notify_NonRootApi(t *testing.T) { Kind: "Master", } - err := client.Notify(t.Context(), zone) + err := client.Notify(context.Background(), zone) require.NoError(t, err) } func TestClient_Notify_v0(t *testing.T) { - client := mockBuilder(). - Route("PUT /some/path/api/v1/servers/localhost/zones/example.org./notify", nil). - Build(t) - + client := setupTest(t, http.MethodPut, "/api/v1/servers/localhost/zones/example.org./notify", http.StatusOK, "") client.apiVersion = 0 zone := &HostedZone{ @@ -386,17 +388,14 @@ func TestClient_Notify_v0(t *testing.T) { Kind: "Master", } - err := client.Notify(t.Context(), zone) + err := client.Notify(context.Background(), zone) require.NoError(t, err) } func TestClient_getAPIVersion(t *testing.T) { - client := mockBuilder(). - Route("GET /api", - servermock.ResponseFromFixture("versions.json")). - Build(t) + client := setupTest(t, http.MethodGet, "/api", http.StatusOK, "versions.json") - version, err := client.getAPIVersion(t.Context()) + version, err := client.getAPIVersion(context.Background()) require.NoError(t, err) assert.Equal(t, 4, version) diff --git a/providers/dns/pdns/internal/fixtures/zone-request.json b/providers/dns/pdns/internal/fixtures/zone-request.json deleted file mode 100644 index 5e4a6d2b9..000000000 --- a/providers/dns/pdns/internal/fixtures/zone-request.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "rrsets": [ - { - "name": "example.org.", - "type": "NS", - "kind": "", - "changetype": "REPLACE", - "records": [ - { - "content": "192.0.2.5", - "disabled": false, - "name": "ns1.example.org.", - "type": "A", - "ttl": 86400 - } - ] - } - ] -} diff --git a/providers/dns/pdns/pdns.go b/providers/dns/pdns/pdns.go index e7ead7078..751501b75 100644 --- a/providers/dns/pdns/pdns.go +++ b/providers/dns/pdns/pdns.go @@ -7,14 +7,11 @@ import ( "fmt" "net/http" "net/url" - "strconv" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/pdns/internal" ) @@ -33,8 +30,6 @@ const ( EnvServerName = envNamespace + "SERVER_NAME" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string @@ -104,12 +99,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client := internal.NewClient(config.Host, config.ServerName, config.APIVersion, config.APIKey) - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - if config.APIVersion <= 0 { err := client.SetAPIVersion(context.Background()) if err != nil { @@ -128,8 +117,6 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) @@ -137,9 +124,11 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("pdns: could not find zone for domain %q: %w", domain, err) } + ctx := context.Background() + zone, err := d.client.GetHostedZone(ctx, authZone) if err != nil { - return fmt.Errorf("pdns: get hosted zone for %s: %w", authZone, err) + return fmt.Errorf("pdns: %w", err) } name := info.EffectiveFQDN @@ -151,49 +140,45 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // Look for existing records. existingRRSet := findTxtRecord(zone, info.EffectiveFQDN) + // merge the existing and new records var records []internal.Record if existingRRSet != nil { records = existingRRSet.Records } - records = append(records, internal.Record{ - Content: strconv.Quote(info.Value), + rec := internal.Record{ + Content: "\"" + info.Value + "\"", Disabled: false, // pre-v1 API Type: "TXT", Name: name, TTL: d.config.TTL, - }) + } rrSets := internal.RRSets{ - RRSets: []internal.RRSet{{ - Name: name, - ChangeType: "REPLACE", - Type: "TXT", - Kind: "Master", - TTL: d.config.TTL, - Records: records, - }}, + RRSets: []internal.RRSet{ + { + Name: name, + ChangeType: "REPLACE", + Type: "TXT", + Kind: "Master", + TTL: d.config.TTL, + Records: append(records, rec), + }, + }, } err = d.client.UpdateRecords(ctx, zone, rrSets) if err != nil { - return fmt.Errorf("pdns: update records: %w", err) + return fmt.Errorf("pdns: %w", err) } - err = d.client.Notify(ctx, zone) - if err != nil { - return fmt.Errorf("pdns: notify: %w", err) - } - - return nil + return d.client.Notify(ctx, zone) } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) @@ -201,49 +186,35 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("pdns: could not find zone for domain %q: %w", domain, err) } + ctx := context.Background() + zone, err := d.client.GetHostedZone(ctx, authZone) if err != nil { - return fmt.Errorf("pdns: get hosted zone for %s: %w", authZone, err) + return fmt.Errorf("pdns: %w", err) } - // Look for existing records. set := findTxtRecord(zone, info.EffectiveFQDN) + if set == nil { return fmt.Errorf("pdns: no existing record found for %s", info.EffectiveFQDN) } - var records []internal.Record - - for _, r := range set.Records { - if r.Content != strconv.Quote(info.Value) { - records = append(records, r) - } + rrSets := internal.RRSets{ + RRSets: []internal.RRSet{ + { + Name: set.Name, + Type: set.Type, + ChangeType: "DELETE", + }, + }, } - rrSet := internal.RRSet{ - Name: set.Name, - Type: set.Type, - } - - if len(records) > 0 { - rrSet.ChangeType = "REPLACE" - rrSet.TTL = d.config.TTL - rrSet.Records = records - } else { - rrSet.ChangeType = "DELETE" - } - - err = d.client.UpdateRecords(ctx, zone, internal.RRSets{RRSets: []internal.RRSet{rrSet}}) + err = d.client.UpdateRecords(ctx, zone, rrSets) if err != nil { - return fmt.Errorf("pdns: update records: %w", err) + return fmt.Errorf("pdns: %w", err) } - err = d.client.Notify(ctx, zone) - if err != nil { - return fmt.Errorf("pdns: notify: %w", err) - } - - return nil + return d.client.Notify(ctx, zone) } func findTxtRecord(zone *internal.HostedZone, fqdn string) *internal.RRSet { diff --git a/providers/dns/pdns/pdns.toml b/providers/dns/pdns/pdns.toml index a83d80922..a59c02cda 100644 --- a/providers/dns/pdns/pdns.toml +++ b/providers/dns/pdns/pdns.toml @@ -7,7 +7,7 @@ Since = "v0.4.0" Example = ''' PDNS_API_URL=http://pdns-server:80/ \ PDNS_API_KEY=xxxx \ -lego --dns pdns -d '*.example.com' -d example.com run +lego --email you@example.com --dns pdns --domains my.example.org run ''' Additional = ''' @@ -28,10 +28,10 @@ PowerDNS Notes: [Configuration.Additional] PDNS_SERVER_NAME = "Name of the server in the URL, 'localhost' by default" PDNS_API_VERSION = "Skip API version autodetection and use the provided version number." - PDNS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - PDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - PDNS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - PDNS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + PDNS_POLLING_INTERVAL = "Time between DNS propagation check" + PDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + PDNS_TTL = "The TTL of the TXT record used for the DNS challenge" + PDNS_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://doc.powerdns.com/md/httpapi/README/" diff --git a/providers/dns/pdns/pdns_test.go b/providers/dns/pdns/pdns_test.go index 0213ba17c..70b386b81 100644 --- a/providers/dns/pdns/pdns_test.go +++ b/providers/dns/pdns/pdns_test.go @@ -57,7 +57,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -137,19 +136,14 @@ func TestLivePresentAndCleanup(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) - err = provider.Present(envTest.GetDomain(), "", "123e==") - require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) - err = provider.CleanUp(envTest.GetDomain(), "", "123e==") - require.NoError(t, err) } func mustParse(rawURL string) *url.URL { @@ -157,6 +151,5 @@ func mustParse(rawURL string) *url.URL { if err != nil { panic(err) } - return u } diff --git a/providers/dns/plesk/internal/client.go b/providers/dns/plesk/internal/client.go index 47abba805..9dd9d5ee3 100644 --- a/providers/dns/plesk/internal/client.go +++ b/providers/dns/plesk/internal/client.go @@ -24,7 +24,7 @@ type Client struct { } // NewClient created a new Client. -func NewClient(baseURL *url.URL, login, password string) *Client { +func NewClient(baseURL *url.URL, login string, password string) *Client { return &Client{ login: login, password: password, @@ -35,7 +35,7 @@ func NewClient(baseURL *url.URL, login, password string) *Client { // GetSite gets a site. // https://docs.plesk.com/en-US/obsidian/api-rpc/about-xml-api/reference/managing-sites-domains/getting-information-about-sites.66583/ -func (c *Client) GetSite(ctx context.Context, domain string) (int, error) { +func (c Client) GetSite(ctx context.Context, domain string) (int, error) { payload := RequestPacketType{Site: &SiteTypeRequest{Get: SiteGetRequest{Filter: &SiteFilterType{ Name: domain, }}}} @@ -62,7 +62,7 @@ func (c *Client) GetSite(ctx context.Context, domain string) (int, error) { // AddRecord adds a TXT record. // https://docs.plesk.com/en-US/obsidian/api-rpc/about-xml-api/reference/managing-dns/managing-dns-records/adding-dns-record.34798/ -func (c *Client) AddRecord(ctx context.Context, siteID int, host, value string) (int, error) { +func (c Client) AddRecord(ctx context.Context, siteID int, host, value string) (int, error) { payload := RequestPacketType{DNS: &DNSInputType{AddRec: []AddRecRequest{{ SiteID: siteID, Type: "TXT", @@ -92,7 +92,7 @@ func (c *Client) AddRecord(ctx context.Context, siteID int, host, value string) // DeleteRecord Deletes a TXT record. // https://docs.plesk.com/en-US/obsidian/api-rpc/about-xml-api/reference/managing-dns/managing-dns-records/deleting-dns-records.34864/ -func (c *Client) DeleteRecord(ctx context.Context, recordID int) (int, error) { +func (c Client) DeleteRecord(ctx context.Context, recordID int) (int, error) { payload := RequestPacketType{DNS: &DNSInputType{DelRec: []DelRecRequest{{Filter: DNSSelectionFilterType{ ID: recordID, }}}}} @@ -117,11 +117,10 @@ func (c *Client) DeleteRecord(ctx context.Context, recordID int) (int, error) { return response.DNS.DelRec[0].Result.ID, nil } -func (c *Client) doRequest(ctx context.Context, payload RequestPacketType) (*ResponsePacketType, error) { +func (c Client) doRequest(ctx context.Context, payload RequestPacketType) (*ResponsePacketType, error) { endpoint := c.baseURL.JoinPath("/enterprise/control/agent.php") body := new(bytes.Buffer) - err := xml.NewEncoder(body).Encode(payload) if err != nil { return nil, err @@ -154,7 +153,6 @@ func (c *Client) doRequest(ctx context.Context, payload RequestPacketType) (*Res } var response ResponsePacketType - err = xml.Unmarshal(raw, &response) if err != nil { return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) diff --git a/providers/dns/plesk/internal/client_test.go b/providers/dns/plesk/internal/client_test.go index 14cadd0e0..5d59a4c87 100644 --- a/providers/dns/plesk/internal/client_test.go +++ b/providers/dns/plesk/internal/client_test.go @@ -1,125 +1,144 @@ package internal import ( + "context" + "fmt" + "io" + "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - serverURL, _ := url.Parse(server.URL) +func setupTest(t *testing.T, filename string) *Client { + t.Helper() - client := NewClient(serverURL, "user", "secret") - client.HTTPClient = server.Client() + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) - return client, nil - }, - servermock.CheckHeader().WithContentType("text/xml"). - With("Http_auth_login", "user"). - With("Http_auth_passwd", "secret"), - ) + serverURL, err := url.Parse(server.URL) + require.NoError(t, err) + + client := NewClient(serverURL, "user", "secret") + client.HTTPClient = server.Client() + + mux.HandleFunc("/enterprise/control/agent.php", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + login := req.Header.Get("Http_auth_login") + if login != "user" { + http.Error(rw, fmt.Sprintf("invalid login: %s", login), http.StatusUnauthorized) + return + } + + password := req.Header.Get("Http_auth_passwd") + if password != "secret" { + http.Error(rw, fmt.Sprintf("invalid password: %s", password), http.StatusUnauthorized) + return + } + + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + return client } func TestClient_GetSite(t *testing.T) { - client := mockBuilder(). - Route("POST /enterprise/control/agent.php", servermock.ResponseFromFixture("get-site.xml")). - Build(t) + client := setupTest(t, "get-site.xml") - siteID, err := client.GetSite(t.Context(), "example.com") + siteID, err := client.GetSite(context.Background(), "example.com") require.NoError(t, err) assert.Equal(t, 82, siteID) } func TestClient_GetSite_error(t *testing.T) { - client := mockBuilder(). - Route("POST /enterprise/control/agent.php", servermock.ResponseFromFixture("get-site-error.xml")). - Build(t) + client := setupTest(t, "get-site-error.xml") - siteID, err := client.GetSite(t.Context(), "example.com") + siteID, err := client.GetSite(context.Background(), "example.com") require.Error(t, err) assert.Equal(t, 0, siteID) } func TestClient_GetSite_system_error(t *testing.T) { - client := mockBuilder(). - Route("POST /enterprise/control/agent.php", servermock.ResponseFromFixture("global-error.xml")). - Build(t) + client := setupTest(t, "global-error.xml") - siteID, err := client.GetSite(t.Context(), "example.com") + siteID, err := client.GetSite(context.Background(), "example.com") require.Error(t, err) assert.Equal(t, 0, siteID) } func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /enterprise/control/agent.php", servermock.ResponseFromFixture("add-record.xml")). - Build(t) + client := setupTest(t, "add-record.xml") - recordID, err := client.AddRecord(t.Context(), 123, "_acme-challenge.example.com", "txtTXTtxt") + recordID, err := client.AddRecord(context.Background(), 123, "_acme-challenge.example.com", "txtTXTtxt") require.NoError(t, err) assert.Equal(t, 4537, recordID) } func TestClient_AddRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /enterprise/control/agent.php", servermock.ResponseFromFixture("add-record-error.xml")). - Build(t) + client := setupTest(t, "add-record-error.xml") - recordID, err := client.AddRecord(t.Context(), 123, "_acme-challenge.example.com", "txtTXTtxt") + recordID, err := client.AddRecord(context.Background(), 123, "_acme-challenge.example.com", "txtTXTtxt") require.ErrorAs(t, err, new(RecResult)) assert.Equal(t, 0, recordID) } func TestClient_AddRecord_system_error(t *testing.T) { - client := mockBuilder(). - Route("POST /enterprise/control/agent.php", servermock.ResponseFromFixture("global-error.xml")). - Build(t) + client := setupTest(t, "global-error.xml") - recordID, err := client.AddRecord(t.Context(), 123, "_acme-challenge.example.com", "txtTXTtxt") + recordID, err := client.AddRecord(context.Background(), 123, "_acme-challenge.example.com", "txtTXTtxt") require.ErrorAs(t, err, new(*System)) assert.Equal(t, 0, recordID) } func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /enterprise/control/agent.php", servermock.ResponseFromFixture("delete-record.xml")). - Build(t) + client := setupTest(t, "delete-record.xml") - recordID, err := client.DeleteRecord(t.Context(), 4537) + recordID, err := client.DeleteRecord(context.Background(), 4537) require.NoError(t, err) assert.Equal(t, 4537, recordID) } func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /enterprise/control/agent.php", servermock.ResponseFromFixture("delete-record-error.xml")). - Build(t) + client := setupTest(t, "delete-record-error.xml") - recordID, err := client.DeleteRecord(t.Context(), 4537) + recordID, err := client.DeleteRecord(context.Background(), 4537) require.ErrorAs(t, err, new(RecResult)) assert.Equal(t, 0, recordID) } func TestClient_DeleteRecord_system_error(t *testing.T) { - client := mockBuilder(). - Route("POST /enterprise/control/agent.php", servermock.ResponseFromFixture("global-error.xml")). - Build(t) + client := setupTest(t, "global-error.xml") - recordID, err := client.DeleteRecord(t.Context(), 4537) + recordID, err := client.DeleteRecord(context.Background(), 4537) require.ErrorAs(t, err, new(*System)) assert.Equal(t, 0, recordID) diff --git a/providers/dns/plesk/plesk.go b/providers/dns/plesk/plesk.go index 5f07dcb50..5b279c5f8 100644 --- a/providers/dns/plesk/plesk.go +++ b/providers/dns/plesk/plesk.go @@ -10,10 +10,8 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/plesk/internal" ) @@ -31,8 +29,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { baseURL string @@ -108,8 +104,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, client: client, @@ -163,7 +157,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() - if !ok { return fmt.Errorf("plesk: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) } @@ -173,9 +166,5 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("plesk: failed to delete record (%d): %w", recordID, err) } - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - return nil } diff --git a/providers/dns/plesk/plesk.toml b/providers/dns/plesk/plesk.toml index 0ef89d6b7..96b507cd7 100644 --- a/providers/dns/plesk/plesk.toml +++ b/providers/dns/plesk/plesk.toml @@ -8,7 +8,7 @@ Example = ''' PLESK_SERVER_BASE_URL="https://plesk.myserver.com:8443" \ PLESK_USERNAME=xxxxxx \ PLESK_PASSWORD=yyyyyy \ -lego --dns plesk -d '*.example.com' -d example.com run +lego --email you@example.com --dns plesk --domains my.example.org run ''' [Configuration] @@ -17,10 +17,10 @@ lego --dns plesk -d '*.example.com' -d example.com run PLESK_USERNAME = "API username" PLESK_PASSWORD = "API password" [Configuration.Additional] - PLESK_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - PLESK_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - PLESK_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - PLESK_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + PLESK_POLLING_INTERVAL = "Time between DNS propagation check" + PLESK_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + PLESK_TTL = "The TTL of the TXT record used for the DNS challenge" + PLESK_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://docs.plesk.com/en-US/obsidian/api-rpc/about-xml-api/reference.28784/" diff --git a/providers/dns/plesk/plesk_test.go b/providers/dns/plesk/plesk_test.go index 506a26a2a..417e2c1da 100644 --- a/providers/dns/plesk/plesk_test.go +++ b/providers/dns/plesk/plesk_test.go @@ -67,7 +67,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -150,7 +149,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -164,7 +162,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/porkbun/porkbun.go b/providers/dns/porkbun/porkbun.go index 2f999ebcc..3df5120fb 100644 --- a/providers/dns/porkbun/porkbun.go +++ b/providers/dns/porkbun/porkbun.go @@ -10,10 +10,8 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/nrdcg/porkbun" ) @@ -32,8 +30,6 @@ const ( const minTTL = 300 -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string @@ -101,8 +97,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, client: client, @@ -154,7 +148,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() - if !ok { return fmt.Errorf("porkbun: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) } @@ -171,10 +164,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("porkbun: failed to delete record: %w", err) } - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - return nil } diff --git a/providers/dns/porkbun/porkbun.toml b/providers/dns/porkbun/porkbun.toml index 9ae036da6..b06f5c300 100644 --- a/providers/dns/porkbun/porkbun.toml +++ b/providers/dns/porkbun/porkbun.toml @@ -1,6 +1,5 @@ Name = "Porkbun" Description = '''''' -# This URL is NOT the API URL. URL = "https://porkbun.com/" Code = "porkbun" Since = "v4.4.0" @@ -8,7 +7,7 @@ Since = "v4.4.0" Example = ''' PORKBUN_SECRET_API_KEY=xxxxxx \ PORKBUN_API_KEY=yyyyyy \ -lego --dns porkbun -d '*.example.com' -d example.com run +lego --email you@example.com --dns porkbun --domains my.example.org run ''' [Configuration] @@ -16,10 +15,10 @@ lego --dns porkbun -d '*.example.com' -d example.com run PORKBUN_SECRET_API_KEY = "secret API key" PORKBUN_API_KEY = "API key" [Configuration.Additional] - PORKBUN_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" - PORKBUN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 600)" - PORKBUN_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - PORKBUN_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + PORKBUN_POLLING_INTERVAL = "Time between DNS propagation check" + PORKBUN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + PORKBUN_TTL = "The TTL of the TXT record used for the DNS challenge" + PORKBUN_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://porkbun.com/api/json/v3/documentation" diff --git a/providers/dns/porkbun/porkbun_test.go b/providers/dns/porkbun/porkbun_test.go index 7c69edfdb..cdf022b5d 100644 --- a/providers/dns/porkbun/porkbun_test.go +++ b/providers/dns/porkbun/porkbun_test.go @@ -54,7 +54,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -125,7 +124,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -139,7 +137,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/rackspace/fixtures/delete.json b/providers/dns/rackspace/fixtures/delete.json deleted file mode 100644 index 7e2f2ac53..000000000 --- a/providers/dns/rackspace/fixtures/delete.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "status": "RUNNING", - "verb": "DELETE", - "jobId": "00000000-0000-0000-0000-0000000000", - "callbackUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/status/00000000-0000-0000-0000-0000000000", - "requestUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/domains/112233/recordsid=TXT-654321" -} diff --git a/providers/dns/rackspace/fixtures/identity.json b/providers/dns/rackspace/fixtures/identity.json deleted file mode 100644 index 5a459d13c..000000000 --- a/providers/dns/rackspace/fixtures/identity.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "access": { - "token": { - "id": "testToken", - "expires": "1970-01-01T00:00:00.000Z", - "tenant": { - "id": "123456", - "name": "123456" - }, - "RAX-AUTH:authenticatedBy": [ - "APIKEY" - ] - }, - "serviceCatalog": [ - { - "type": "rax:dns", - "endpoints": [ - { - "publicURL": "https://dns.api.rackspacecloud.com/v1.0/123456", - "tenantId": "123456" - } - ], - "name": "cloudDNS" - } - ], - "user": { - "id": "fakeUseID", - "name": "testUser" - } - } -} diff --git a/providers/dns/rackspace/fixtures/record.json b/providers/dns/rackspace/fixtures/record.json deleted file mode 100644 index 4d76aa0c8..000000000 --- a/providers/dns/rackspace/fixtures/record.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "request": "{\"records\":[{\"name\":\"_acme-challenge.example.com\",\"type\":\"TXT\",\"data\":\"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM\",\"ttl\":300}]}", - "status": "RUNNING", - "verb": "POST", - "jobId": "00000000-0000-0000-0000-0000000000", - "callbackUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/status/00000000-0000-0000-0000-0000000000", - "requestUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/domains/112233/records" -} diff --git a/providers/dns/rackspace/fixtures/record_details.json b/providers/dns/rackspace/fixtures/record_details.json deleted file mode 100644 index e53cf1330..000000000 --- a/providers/dns/rackspace/fixtures/record_details.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "records": [ - { - "name": "_acme-challenge.example.com", - "id": "TXT-654321", - "type": "TXT", - "data": "pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM", - "ttl": 300, - "updated": "1970-01-01T00:00:00.000+0000", - "created": "1970-01-01T00:00:00.000+0000" - } - ] -} diff --git a/providers/dns/rackspace/fixtures/zone_details.json b/providers/dns/rackspace/fixtures/zone_details.json deleted file mode 100644 index f68f23aa0..000000000 --- a/providers/dns/rackspace/fixtures/zone_details.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "domains": [ - { - "name": "example.com", - "id": "112233", - "emailAddress": "hostmaster@example.com", - "updated": "1970-01-01T00:00:00.000+0000", - "created": "1970-01-01T00:00:00.000+0000" - } - ], - "totalEntries": 1 -} diff --git a/providers/dns/rackspace/internal/client.go b/providers/dns/rackspace/internal/client.go index 4a1872484..cbfdd1bfa 100644 --- a/providers/dns/rackspace/internal/client.go +++ b/providers/dns/rackspace/internal/client.go @@ -14,8 +14,6 @@ import ( "github.com/go-acme/lego/v4/providers/dns/internal/errutils" ) -const AuthToken = "X-Auth-Token" - type Client struct { token string @@ -23,7 +21,7 @@ type Client struct { HTTPClient *http.Client } -func NewClient(endpoint, token string) (*Client, error) { +func NewClient(endpoint string, token string) (*Client, error) { baseURL, err := url.Parse(endpoint) if err != nil { return nil, err @@ -36,7 +34,7 @@ func NewClient(endpoint, token string) (*Client, error) { }, nil } -// AddRecord Adds one record to a specified domain. +// AddRecord Adds one record to a specified domain. // https://docs.rackspace.com/docs/cloud-dns/v1/api-reference/records#add-records func (c *Client) AddRecord(ctx context.Context, zoneID string, record Record) error { endpoint := c.baseURL.JoinPath("domains", zoneID, "records") @@ -113,7 +111,6 @@ func (c *Client) listDomainsByName(ctx context.Context, domain string) (*ZoneSea } var zoneSearchResponse ZoneSearchResponse - err = c.do(req, &zoneSearchResponse) if err != nil { return nil, err @@ -123,7 +120,7 @@ func (c *Client) listDomainsByName(ctx context.Context, domain string) (*ZoneSea } // FindTxtRecord searches a DNS zone for a TXT record with a specific name. -func (c *Client) FindTxtRecord(ctx context.Context, fqdn, zoneID string) (*Record, error) { +func (c *Client) FindTxtRecord(ctx context.Context, fqdn string, zoneID string) (*Record, error) { records, err := c.searchRecords(ctx, zoneID, dns01.UnFqdn(fqdn), "TXT") if err != nil { return nil, err @@ -155,7 +152,6 @@ func (c *Client) searchRecords(ctx context.Context, zoneID, recordName, recordTy } var records Records - err = c.do(req, &records) if err != nil { return nil, err @@ -165,7 +161,7 @@ func (c *Client) searchRecords(ctx context.Context, zoneID, recordName, recordTy } func (c *Client) do(req *http.Request, result any) error { - req.Header.Set(AuthToken, c.token) + req.Header.Set("X-Auth-Token", c.token) resp, err := c.HTTPClient.Do(req) if err != nil { @@ -195,7 +191,7 @@ func (c *Client) do(req *http.Request, result any) error { return nil } -func newJSONRequest[T string | *url.URL](ctx context.Context, method string, endpoint T, payload any) (*http.Request, error) { +func newJSONRequest[T string | *url.URL](ctx context.Context, method string, endpoint T, payload interface{}) (*http.Request, error) { buf := new(bytes.Buffer) if payload != nil { diff --git a/providers/dns/rackspace/internal/client_test.go b/providers/dns/rackspace/internal/client_test.go index c14c4d360..993d34d9f 100644 --- a/providers/dns/rackspace/internal/client_test.go +++ b/providers/dns/rackspace/internal/client_test.go @@ -1,64 +1,81 @@ package internal import ( + "context" + "fmt" + "io" + "net/http" "net/http/httptest" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient(server.URL, "secret") - if err != nil { - return nil, err - } +func setupTest(t *testing.T, pattern string, handler http.HandlerFunc) *Client { + t.Helper() - client.HTTPClient = server.Client() + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - With(AuthToken, "secret")) + client, err := NewClient(server.URL, "secret") + require.NoError(t, err) + + client.HTTPClient = server.Client() + + mux.HandleFunc(pattern, handler) + + return client +} + +func writeFixtureHandler(method, filename string) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + if req.Header.Get("X-Auth-Token") != "secret" { + http.Error(rw, fmt.Sprintf("invalid token: %q", req.Header.Get("X-Auth-Token")), http.StatusUnauthorized) + return + } + + if filename == "" { + return + } + + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, _ = io.Copy(rw, file) + } } func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/1234/records", - servermock.ResponseFromFixture("add-records.json"), - servermock.CheckRequestJSONBody(`{"records":[{"name":"exmaple.com","type":"TXT","data":"value1","ttl":120,"id":"abc"}]}`)). - Build(t) + client := setupTest(t, "/domains/1234/records", writeFixtureHandler(http.MethodPost, "add-records.json")) - record := Record{ - Name: "exmaple.com", - Type: "TXT", - Data: "value1", - TTL: 120, - ID: "abc", - } - - err := client.AddRecord(t.Context(), "1234", record) + err := client.AddRecord(context.Background(), "1234", Record{}) require.NoError(t, err) } func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /domains/1234/records", nil). - Build(t) + client := setupTest(t, "/domains/1234/records", writeFixtureHandler(http.MethodDelete, "")) - err := client.DeleteRecord(t.Context(), "1234", "2725233") + err := client.DeleteRecord(context.Background(), "1234", "2725233") require.NoError(t, err) } func TestClient_searchRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /domains/1234/records", servermock.ResponseFromFixture("search-records.json")). - Build(t) + client := setupTest(t, "/domains/1234/records", writeFixtureHandler(http.MethodGet, "search-records.json")) - records, err := client.searchRecords(t.Context(), "1234", "2725233", "A") + records, err := client.searchRecords(context.Background(), "1234", "2725233", "A") require.NoError(t, err) expected := &Records{ @@ -77,11 +94,9 @@ func TestClient_searchRecords(t *testing.T) { } func TestClient_listDomainsByName(t *testing.T) { - client := mockBuilder(). - Route("GET /domains", servermock.ResponseFromFixture("list-domains-by-name.json")). - Build(t) + client := setupTest(t, "/domains", writeFixtureHandler(http.MethodGet, "list-domains-by-name.json")) - domains, err := client.listDomainsByName(t.Context(), "1234") + domains, err := client.listDomainsByName(context.Background(), "1234") require.NoError(t, err) expected := &ZoneSearchResponse{ diff --git a/providers/dns/rackspace/internal/identity.go b/providers/dns/rackspace/internal/identity.go index 3ff667fb8..062350df5 100644 --- a/providers/dns/rackspace/internal/identity.go +++ b/providers/dns/rackspace/internal/identity.go @@ -65,7 +65,6 @@ func (a *Identifier) Login(ctx context.Context, apiUser, apiKey string) (*Identi } var identity Identity - err = json.Unmarshal(raw, &identity) if err != nil { return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) diff --git a/providers/dns/rackspace/internal/identity_test.go b/providers/dns/rackspace/internal/identity_test.go index 44a8d75fc..9ba5abb50 100644 --- a/providers/dns/rackspace/internal/identity_test.go +++ b/providers/dns/rackspace/internal/identity_test.go @@ -1,24 +1,51 @@ package internal import ( + "context" + "fmt" + "io" + "net/http" "net/http/httptest" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func setupIdentifier(server *httptest.Server) (*Identifier, error) { - return NewIdentifier(server.Client(), server.URL), nil +func writeIdentityFixtureHandler(method, filename string) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + if filename == "" { + return + } + + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, _ = io.Copy(rw, file) + } } func TestIdentifier_Login(t *testing.T) { - identifier := servermock.NewBuilder[*Identifier](setupIdentifier, servermock.CheckHeader().WithJSONHeaders()). - Route("POST /", servermock.ResponseFromFixture("tokens.json")). - Build(t) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) - identity, err := identifier.Login(t.Context(), "user", "secret") + identifier := NewIdentifier(server.Client(), server.URL) + + mux.HandleFunc("/", writeIdentityFixtureHandler(http.MethodPost, "tokens.json")) + + identity, err := identifier.Login(context.Background(), "user", "secret") require.NoError(t, err) expected := &Identity{ diff --git a/providers/dns/rackspace/rackspace.go b/providers/dns/rackspace/rackspace.go index b4c7b4a0f..c877de3b8 100644 --- a/providers/dns/rackspace/rackspace.go +++ b/providers/dns/rackspace/rackspace.go @@ -8,10 +8,8 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/rackspace/internal" ) @@ -28,8 +26,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string @@ -99,7 +95,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Iterate through the Service Catalog to get the DNS Endpoint var dnsEndpoint string - for _, service := range identity.Access.ServiceCatalog { if service.Name == "cloudDNS" { dnsEndpoint = service.Endpoints[0].PublicURL @@ -120,8 +115,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, client: client, diff --git a/providers/dns/rackspace/rackspace.toml b/providers/dns/rackspace/rackspace.toml index 0a4a80ffc..35768b4ed 100644 --- a/providers/dns/rackspace/rackspace.toml +++ b/providers/dns/rackspace/rackspace.toml @@ -7,7 +7,7 @@ Since = "v0.4.0" Example = ''' RACKSPACE_USER=xxxx \ RACKSPACE_API_KEY=yyyy \ -lego --dns rackspace -d '*.example.com' -d example.com run +lego --email you@example.com --dns rackspace --domains my.example.org run ''' [Configuration] @@ -15,10 +15,10 @@ lego --dns rackspace -d '*.example.com' -d example.com run RACKSPACE_USER = "API user" RACKSPACE_API_KEY = "API key" [Configuration.Additional] - RACKSPACE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 3)" - RACKSPACE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - RACKSPACE_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - RACKSPACE_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + RACKSPACE_POLLING_INTERVAL = "Time between DNS propagation check" + RACKSPACE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + RACKSPACE_TTL = "The TTL of the TXT record used for the DNS challenge" + RACKSPACE_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://developer.rackspace.com/docs/cloud-dns/v1/" diff --git a/providers/dns/rackspace/rackspace_mock_test.go b/providers/dns/rackspace/rackspace_mock_test.go new file mode 100644 index 000000000..790d52498 --- /dev/null +++ b/providers/dns/rackspace/rackspace_mock_test.go @@ -0,0 +1,87 @@ +package rackspace + +const recordDeleteMock = ` +{ + "status": "RUNNING", + "verb": "DELETE", + "jobId": "00000000-0000-0000-0000-0000000000", + "callbackUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/status/00000000-0000-0000-0000-0000000000", + "requestUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/domains/112233/recordsid=TXT-654321" +} +` + +const recordDetailsMock = ` +{ + "records": [ + { + "name": "_acme-challenge.example.com", + "id": "TXT-654321", + "type": "TXT", + "data": "pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM", + "ttl": 300, + "updated": "1970-01-01T00:00:00.000+0000", + "created": "1970-01-01T00:00:00.000+0000" + } + ] +} +` + +const zoneDetailsMock = ` +{ + "domains": [ + { + "name": "example.com", + "id": "112233", + "emailAddress": "hostmaster@example.com", + "updated": "1970-01-01T00:00:00.000+0000", + "created": "1970-01-01T00:00:00.000+0000" + } + ], + "totalEntries": 1 +} +` + +const identityResponseMock = ` +{ + "access": { + "token": { + "id": "testToken", + "expires": "1970-01-01T00:00:00.000Z", + "tenant": { + "id": "123456", + "name": "123456" + }, + "RAX-AUTH:authenticatedBy": [ + "APIKEY" + ] + }, + "serviceCatalog": [ + { + "type": "rax:dns", + "endpoints": [ + { + "publicURL": "https://dns.api.rackspacecloud.com/v1.0/123456", + "tenantId": "123456" + } + ], + "name": "cloudDNS" + } + ], + "user": { + "id": "fakeUseID", + "name": "testUser" + } + } +} +` + +const recordResponseMock = ` +{ + "request": "{\"records\":[{\"name\":\"_acme-challenge.example.com\",\"type\":\"TXT\",\"data\":\"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM\",\"ttl\":300}]}", + "status": "RUNNING", + "verb": "POST", + "jobId": "00000000-0000-0000-0000-0000000000", + "callbackUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/status/00000000-0000-0000-0000-0000000000", + "requestUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/domains/112233/records" +} +` diff --git a/providers/dns/rackspace/rackspace_test.go b/providers/dns/rackspace/rackspace_test.go index de0749fd3..cbc57b472 100644 --- a/providers/dns/rackspace/rackspace_test.go +++ b/providers/dns/rackspace/rackspace_test.go @@ -1,7 +1,9 @@ package rackspace import ( + "bytes" "fmt" + "io" "net/http" "net/http/httptest" "strings" @@ -9,7 +11,6 @@ import ( "time" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -22,7 +23,11 @@ var envTest = tester.NewEnvTest( WithDomain(envDomain) func TestNewDNSProviderConfig(t *testing.T) { - provider := mockBuilder().Build(t) + config := setupTest(t) + + provider, err := NewDNSProviderConfig(config) + require.NoError(t, err) + assert.NotNil(t, provider.config) assert.Equal(t, "testToken", provider.token, "The token should match") } @@ -33,40 +38,25 @@ func TestNewDNSProviderConfig_MissingCredErr(t *testing.T) { } func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("GET /123456/domains", - servermock.ResponseFromFixture("zone_details.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "example.com")). - Route("POST /123456/domains/112233/records", - servermock.ResponseFromFixture("record.json"). - WithStatusCode(http.StatusAccepted), - servermock.CheckRequestJSONBody(`{"records":[{"name":"_acme-challenge.example.com","type":"TXT","data":"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM","ttl":300}]}`)). - Build(t) + config := setupTest(t) - err := provider.Present("example.com", "token", "keyAuth") - require.NoError(t, err) + provider, err := NewDNSProviderConfig(config) + + if assert.NoError(t, err) { + err = provider.Present("example.com", "token", "keyAuth") + require.NoError(t, err) + } } func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("GET /123456/domains", - servermock.ResponseFromFixture("zone_details.json"), - servermock.CheckQueryParameter().Strict(). - With("name", "example.com")). - Route("GET /123456/domains/112233/records", - servermock.ResponseFromFixture("record_details.json"), - servermock.CheckQueryParameter().Strict(). - With("type", "TXT"). - With("name", "_acme-challenge.example.com")). - Route("DELETE /123456/domains/112233/records", - servermock.ResponseFromFixture("delete.json"), - servermock.CheckQueryParameter().Strict(). - With("id", "TXT-654321")). - Build(t) + config := setupTest(t) - err := provider.CleanUp("example.com", "token", "keyAuth") - require.NoError(t, err) + provider, err := NewDNSProviderConfig(config) + + if assert.NoError(t, err) { + err = provider.CleanUp("example.com", "token", "keyAuth") + require.NoError(t, err) + } } func TestLiveNewDNSProvider_ValidEnv(t *testing.T) { @@ -75,7 +65,6 @@ func TestLiveNewDNSProvider_ValidEnv(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -88,7 +77,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -102,7 +90,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -112,60 +99,99 @@ func TestLiveCleanUp(t *testing.T) { require.NoError(t, err) } -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.HTTPClient = server.Client() - config.APIUser = "testUser" - config.APIKey = "testKey" - config.HTTPClient = server.Client() - config.BaseURL = server.URL + "/v2.0/tokens" +func setupTest(t *testing.T) *Config { + t.Helper() - return NewDNSProviderConfig(config) - }, - servermock.CheckHeader().WithJSONHeaders(), - ). - Route("POST /v2.0/tokens", - http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - apiURL := fmt.Sprintf("http://%s/123456", req.Context().Value(http.LocalAddrContextKey)) + dnsAPI := httptest.NewServer(dnsHandler()) + t.Cleanup(dnsAPI.Close) - resp := strings.Replace(` -{ - "access": { - "token": { - "id": "testToken", - "expires": "1970-01-01T00:00:00.000Z", - "tenant": { - "id": "123456", - "name": "123456" - }, - "RAX-AUTH:authenticatedBy": [ - "APIKEY" - ] - }, - "serviceCatalog": [ - { - "type": "rax:dns", - "endpoints": [ - { - "publicURL": "https://dns.api.rackspacecloud.com/v1.0/123456", - "tenantId": "123456" - } - ], - "name": "cloudDNS" - } - ], - "user": { - "id": "fakeUseID", - "name": "testUser" - } - } + identityAPI := httptest.NewServer(identityHandler(dnsAPI.URL + "/123456")) + t.Cleanup(identityAPI.Close) + + config := NewDefaultConfig() + config.APIUser = "testUser" + config.APIKey = "testKey" + config.HTTPClient = identityAPI.Client() + config.BaseURL = identityAPI.URL + "/" + + return config } -`, "https://dns.api.rackspacecloud.com/v1.0/123456", apiURL, 1) - rw.WriteHeader(http.StatusOK) - _, _ = fmt.Fprint(rw, resp) - }), - servermock.CheckRequestJSONBody(`{"auth":{"RAX-KSKEY:apiKeyCredentials":{"username":"testUser","apiKey":"testKey"}}}`)) +func identityHandler(dnsEndpoint string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + reqBody, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + if string(bytes.TrimSpace(reqBody)) != `{"auth":{"RAX-KSKEY:apiKeyCredentials":{"username":"testUser","apiKey":"testKey"}}}` { + http.Error(w, fmt.Sprintf("invalid body: %s", string(reqBody)), http.StatusBadRequest) + return + } + + resp := strings.Replace(identityResponseMock, "https://dns.api.rackspacecloud.com/v1.0/123456", dnsEndpoint, 1) + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprint(w, resp) + }) +} + +func dnsHandler() *http.ServeMux { + mux := http.NewServeMux() + + // Used by `getHostedZoneID()` finding `zoneID` "?name=example.com" + mux.HandleFunc("/123456/domains", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("name") == "example.com" { + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprint(w, zoneDetailsMock) + return + } + w.WriteHeader(http.StatusBadRequest) + }) + + mux.HandleFunc("/123456/domains/112233/records", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + // Used by `Present()` creating the TXT record + case http.MethodPost: + reqBody, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if string(bytes.TrimSpace(reqBody)) != `{"records":[{"name":"_acme-challenge.example.com","type":"TXT","data":"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM","ttl":300}]}` { + http.Error(w, fmt.Sprintf("invalid body: %s", string(reqBody)), http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusAccepted) + _, _ = fmt.Fprint(w, recordResponseMock) + + // Used by `findTxtRecord()` finding `record.ID` "?type=TXT&name=_acme-challenge.example.com" + case http.MethodGet: + if r.URL.Query().Get("type") == "TXT" && r.URL.Query().Get("name") == "_acme-challenge.example.com" { + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprint(w, recordDetailsMock) + return + } + + w.WriteHeader(http.StatusBadRequest) + return + + // Used by `CleanUp()` deleting the TXT record "?id=445566" + case http.MethodDelete: + if r.URL.Query().Get("id") == "TXT-654321" { + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprint(w, recordDeleteMock) + return + } + w.WriteHeader(http.StatusBadRequest) + } + }) + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + http.Error(w, fmt.Sprintf("Not Found for Request: (%+v)", r), http.StatusNotFound) + }) + + return mux } diff --git a/providers/dns/rainyun/internal/client.go b/providers/dns/rainyun/internal/client.go deleted file mode 100644 index 595b39f29..000000000 --- a/providers/dns/rainyun/internal/client.go +++ /dev/null @@ -1,184 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - querystring "github.com/google/go-querystring/query" -) - -const defaultBaseURL = "https://api.v2.rainyun.com/product/" - -// Client the Rain Yun API client. -type Client struct { - apiKey string - - baseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(apiKey string) (*Client, error) { - if apiKey == "" { - return nil, errors.New("credentials missing") - } - - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - apiKey: apiKey, - baseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -func (c *Client) AddRecord(ctx context.Context, domainID int, record Record) error { - endpoint := c.baseURL.JoinPath("domain", strconv.Itoa(domainID), "dns") - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) - if err != nil { - return err - } - - return c.do(req, nil) -} - -func (c *Client) DeleteRecord(ctx context.Context, domainID, recordID int) error { - endpoint := c.baseURL.JoinPath("domain", strconv.Itoa(domainID), "dns") - - values, err := querystring.Values(Record{ID: recordID}) - if err != nil { - return err - } - - endpoint.RawQuery = values.Encode() - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return err - } - - return c.do(req, nil) -} - -func (c *Client) ListRecords(ctx context.Context, domainID int) ([]Record, error) { - endpoint := c.baseURL.JoinPath("domain", strconv.Itoa(domainID), "dns") - - query := endpoint.Query() - query.Set("limit", "100") - query.Set("page_no", "1") - endpoint.RawQuery = query.Encode() - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var recordData APIResponse[Record] - - err = c.do(req, &recordData) - if err != nil { - return nil, err - } - - return recordData.Data.Records, nil -} - -func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) { - endpoint := c.baseURL.JoinPath("domain") - - query := endpoint.Query() - query.Set("options", `{"columnFilters":{"domains.Domain":""},"sort":[],"page":1,"perPage":100}`) - endpoint.RawQuery = query.Encode() - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var domainData APIResponse[Domain] - - err = c.do(req, &domainData) - if err != nil { - return nil, err - } - - return domainData.Data.Records, nil -} - -func (c *Client) do(req *http.Request, result any) error { - req.Header.Add("x-api-key", c.apiKey) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIError - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return &errAPI -} diff --git a/providers/dns/rainyun/internal/client_test.go b/providers/dns/rainyun/internal/client_test.go deleted file mode 100644 index 8246001af..000000000 --- a/providers/dns/rainyun/internal/client_test.go +++ /dev/null @@ -1,167 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("secret") - if err != nil { - return nil, err - } - - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders()) -} - -func TestClient_ListDomains(t *testing.T) { - client := mockBuilder(). - Route("GET /domain", - servermock.ResponseFromFixture("domains.json"), - servermock.CheckQueryParameter().Strict(). - With("options", `{"columnFilters":{"domains.Domain":""},"sort":[],"page":1,"perPage":100}`)). - Build(t) - - domains, err := client.ListDomains(t.Context()) - require.NoError(t, err) - - expected := []Domain{ - {ID: 1, Domain: "example.com"}, - {ID: 2, Domain: "example.org"}, - } - - assert.Equal(t, expected, domains) -} - -func TestClient_ListDomains_error(t *testing.T) { - client := mockBuilder(). - Route("GET /domain", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusForbidden)). - Build(t) - - _, err := client.ListDomains(t.Context()) - require.Error(t, err) - - assert.EqualError(t, err, "30039: 密钥认证错误或已失效") -} - -func TestClient_ListRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /domain/123/dns", - servermock.ResponseFromFixture("records.json"), - servermock.CheckQueryParameter().Strict(). - With("limit", "100"). - With("page_no", "1")). - Build(t) - - records, err := client.ListRecords(t.Context(), 123) - require.NoError(t, err) - - expected := []Record{ - { - ID: 1, - Host: "_acme-challenge.foo.example.com", - Line: "DEFAULT", - TTL: 120, - Type: "TXT", - Value: "foo", - }, - { - ID: 2, - Host: "_acme-challenge.bar.example.com", - Line: "DEFAULT", - TTL: 300, - Type: "TXT", - Value: "bar", - }, - } - - assert.Equal(t, expected, records) -} - -func TestClient_ListRecords_error(t *testing.T) { - client := mockBuilder(). - Route("GET /domain/123/dns", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusForbidden)). - Build(t) - - _, err := client.ListRecords(t.Context(), 123) - require.Error(t, err) - - assert.EqualError(t, err, "30039: 密钥认证错误或已失效") -} - -func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /domain/123/dns", nil). - Build(t) - - record := Record{ - Host: "_acme-challenge.foo.example.com", - Line: "DEFAULT", - TTL: 120, - Type: "TXT", - Value: "foo", - } - - err := client.AddRecord(t.Context(), 123, record) - require.NoError(t, err) -} - -func TestClient_AddRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /domain/123/dns", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusForbidden)). - Build(t) - - record := Record{ - Host: "_acme-challenge.foo.example.com", - Line: "DEFAULT", - TTL: 120, - Type: "TXT", - Value: "foo", - } - - err := client.AddRecord(t.Context(), 123, record) - require.Error(t, err) - - assert.EqualError(t, err, "30039: 密钥认证错误或已失效") -} - -func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /domain/123/dns", nil). - Build(t) - - err := client.DeleteRecord(t.Context(), 123, 456) - require.NoError(t, err) -} - -func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /domain/123/dns", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusForbidden)). - Build(t) - - err := client.DeleteRecord(t.Context(), 123, 456) - require.Error(t, err) - - assert.EqualError(t, err, "30039: 密钥认证错误或已失效") -} diff --git a/providers/dns/rainyun/internal/fixtures/domains.json b/providers/dns/rainyun/internal/fixtures/domains.json deleted file mode 100644 index 930e4e189..000000000 --- a/providers/dns/rainyun/internal/fixtures/domains.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "code": 0, - "data": { - "TotalRecords": 2, - "Records": [ - { - "id": 1, - "domain": "example.com" - }, - { - "id": 2, - "domain": "example.org" - } - ] - } -} diff --git a/providers/dns/rainyun/internal/fixtures/error.json b/providers/dns/rainyun/internal/fixtures/error.json deleted file mode 100644 index 31e9f7138..000000000 --- a/providers/dns/rainyun/internal/fixtures/error.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "code": 30039, - "message": "密钥认证错误或已失效" -} diff --git a/providers/dns/rainyun/internal/fixtures/records.json b/providers/dns/rainyun/internal/fixtures/records.json deleted file mode 100644 index d24c0c9ec..000000000 --- a/providers/dns/rainyun/internal/fixtures/records.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "code": 0, - "data": { - "TotalRecords": 2, - "Records": [ - { - "record_id": 1, - "host": "_acme-challenge.foo.example.com", - "type": "TXT", - "TTL": 120, - "value": "foo", - "line": "DEFAULT" - }, - { - "record_id": 2, - "host": "_acme-challenge.bar.example.com", - "type": "TXT", - "TTL": 300, - "value": "bar", - "line": "DEFAULT" - } - ] - } -} diff --git a/providers/dns/rainyun/internal/types.go b/providers/dns/rainyun/internal/types.go deleted file mode 100644 index 8ce559112..000000000 --- a/providers/dns/rainyun/internal/types.go +++ /dev/null @@ -1,37 +0,0 @@ -package internal - -import "fmt" - -type APIError struct { - Code int `json:"code"` - Message string `json:"message"` -} - -func (a *APIError) Error() string { - return fmt.Sprintf("%d: %s", a.Code, a.Message) -} - -type Record struct { - ID int `json:"record_id,omitempty" url:"record_id,omitempty"` - Host string `json:"host,omitempty" url:"host,omitempty"` - Priority int `json:"level,omitempty" url:"level,omitempty"` - Line string `json:"line,omitempty" url:"line,omitempty"` - TTL int `json:"ttl,omitempty" url:"ttl,omitempty"` - Type string `json:"type,omitempty" url:"type,omitempty"` - Value string `json:"value,omitempty" url:"value,omitempty"` -} - -type Domain struct { - ID int `json:"id,omitempty"` - Domain string `json:"domain,omitempty"` -} - -type APIResponse[T any] struct { - Code int `json:"code"` - Data *Data[T] `json:"data"` -} - -type Data[T any] struct { - TotalRecords int `json:"TotalRecords"` - Records []T `json:"Records"` -} diff --git a/providers/dns/rainyun/rainyun.go b/providers/dns/rainyun/rainyun.go deleted file mode 100644 index a4d1c4035..000000000 --- a/providers/dns/rainyun/rainyun.go +++ /dev/null @@ -1,200 +0,0 @@ -// Package rainyun implements a DNS provider for solving the DNS-01 challenge using Rain Yun. -package rainyun - -import ( - "context" - "errors" - "fmt" - "net/http" - "strings" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/rainyun/internal" -) - -// Environment variables names. -const ( - envNamespace = "RAINYUN_" - - EnvAPIKey = envNamespace + "API_KEY" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for Rain Yun. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIKey) - if err != nil { - return nil, fmt.Errorf("rainyun: %w", err) - } - - config := NewDefaultConfig() - config.APIKey = values[EnvAPIKey] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Rain Yun. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("rainyun: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.APIKey) - if err != nil { - return nil, fmt.Errorf("rainyun: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx := context.Background() - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("rainyun: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("rainyun: %w", err) - } - - domainID, err := d.findDomainID(ctx, dns01.UnFqdn(authZone)) - if err != nil { - return fmt.Errorf("rainyun: find domain ID: %w", err) - } - - record := internal.Record{ - Host: subDomain, - Priority: 10, - Line: "DEFAULT", - TTL: d.config.TTL, - Type: "TXT", - Value: info.Value, - } - - err = d.client.AddRecord(ctx, domainID, record) - if err != nil { - return fmt.Errorf("rainyun: add record: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - ctx := context.Background() - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("rainyun: could not find zone for domain %q: %w", domain, err) - } - - domainID, err := d.findDomainID(ctx, dns01.UnFqdn(authZone)) - if err != nil { - return fmt.Errorf("rainyun: find domain ID: %w", err) - } - - recordID, err := d.findRecordID(ctx, domainID, info) - if err != nil { - return fmt.Errorf("rainyun: find record ID: %w", err) - } - - err = d.client.DeleteRecord(ctx, domainID, recordID) - if err != nil { - return fmt.Errorf("rainyun: delete record: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -func (d *DNSProvider) findDomainID(ctx context.Context, domain string) (int, error) { - domains, err := d.client.ListDomains(ctx) - if err != nil { - return 0, err - } - - for _, dom := range domains { - if dom.Domain == domain { - return dom.ID, nil - } - } - - return 0, fmt.Errorf("domain not found: %s", domain) -} - -func (d *DNSProvider) findRecordID(ctx context.Context, domainID int, info dns01.ChallengeInfo) (int, error) { - records, err := d.client.ListRecords(ctx, domainID) - if err != nil { - return 0, fmt.Errorf("list records: %w", err) - } - - zone := dns01.UnFqdn(info.EffectiveFQDN) - - for _, record := range records { - if strings.HasPrefix(zone, record.Host) && record.Value == info.Value { - return record.ID, nil - } - } - - return 0, fmt.Errorf("record not found: domainID=%d, fqdn=%s", domainID, info.EffectiveFQDN) -} diff --git a/providers/dns/rainyun/rainyun.toml b/providers/dns/rainyun/rainyun.toml deleted file mode 100644 index fe2b3c07d..000000000 --- a/providers/dns/rainyun/rainyun.toml +++ /dev/null @@ -1,22 +0,0 @@ -Name = "Rain Yun/雨云" -Description = '''''' -URL = "https://www.rainyun.com" -Code = "rainyun" -Since = "v4.21.0" - -Example = ''' -RAINYUN_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns rainyun -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - RAINYUN_API_KEY = "API key" - [Configuration.Additional] - RAINYUN_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - RAINYUN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - RAINYUN_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - RAINYUN_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://www.apifox.cn/apidoc/shared-a4595cc8-44c5-4678-a2a3-eed7738dab03/api-151416609" diff --git a/providers/dns/rainyun/rainyun_test.go b/providers/dns/rainyun/rainyun_test.go deleted file mode 100644 index d27d47e81..000000000 --- a/providers/dns/rainyun/rainyun_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package rainyun - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAPIKey: "secret", - }, - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "rainyun: some credentials information are missing: RAINYUN_API_KEY", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiKey string - expected string - }{ - { - desc: "success", - apiKey: "secret", - }, - { - desc: "missing credentials", - expected: "rainyun: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIKey = test.apiKey - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/rcodezero/internal/client.go b/providers/dns/rcodezero/internal/client.go index 5cf39907e..d37fec2dd 100644 --- a/providers/dns/rcodezero/internal/client.go +++ b/providers/dns/rcodezero/internal/client.go @@ -64,7 +64,6 @@ func (c *Client) do(req *http.Request) (*APIResponse, error) { } result := &APIResponse{} - raw, err := io.ReadAll(resp.Body) if err != nil { return nil, errutils.NewReadResponseError(req, resp.StatusCode, err) @@ -106,7 +105,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) errAPI := &APIResponse{} - err := json.Unmarshal(raw, errAPI) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/rcodezero/internal/client_test.go b/providers/dns/rcodezero/internal/client_test.go index b70107072..096786084 100644 --- a/providers/dns/rcodezero/internal/client_test.go +++ b/providers/dns/rcodezero/internal/client_test.go @@ -1,30 +1,69 @@ package internal import ( + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func setupClient(server *httptest.Server) (*Client, error) { +func setupTest(t *testing.T, method, pattern string, status int, file string) *Client { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest) + return + } + + apiToken := req.Header.Get(authorizationHeader) + if apiToken != "Bearer secret" { + http.Error(rw, fmt.Sprintf("invalid credentials: %s", apiToken), http.StatusBadRequest) + return + } + + if file == "" { + rw.WriteHeader(status) + return + } + + open, err := os.Open(filepath.Join("fixtures", file)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = open.Close() }() + + rw.WriteHeader(status) + _, err = io.Copy(rw, open) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + client := NewClient("secret") client.HTTPClient = server.Client() client.baseURL, _ = url.Parse(server.URL) - return client, nil + return client } func TestClient_UpdateRecords_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient, servermock.CheckHeader().WithJSONHeaders()). - Route("PATCH /v1/acme/zones/example.org/rrsets", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnprocessableEntity)). - Build(t) + client := setupTest(t, http.MethodPatch, "/v1/acme/zones/example.org/rrsets", http.StatusUnprocessableEntity, "error.json") rrSet := []UpdateRRSet{{ Name: "acme.example.org.", @@ -33,16 +72,13 @@ func TestClient_UpdateRecords_error(t *testing.T) { Records: []Record{{Content: `"my-acme-challenge"`}}, }} - resp, err := client.UpdateRecords(t.Context(), "example.org", rrSet) + resp, err := client.UpdateRecords(context.Background(), "example.org", rrSet) require.ErrorAs(t, err, new(*APIResponse)) - assert.Nil(t, resp) + assert.Nil(t, resp) //nolint:testifylint // false positive https://github.com/Antonboom/testifylint/issues/95 } func TestClient_UpdateRecords(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient, servermock.CheckHeader().WithJSONHeaders()). - Route("PATCH /v1/acme/zones/example.org/rrsets", - servermock.ResponseFromFixture("rrsets-response.json")). - Build(t) + client := setupTest(t, http.MethodPatch, "/v1/acme/zones/example.org/rrsets", http.StatusOK, "rrsets-response.json") rrSet := []UpdateRRSet{{ Name: "acme.example.org.", @@ -51,7 +87,7 @@ func TestClient_UpdateRecords(t *testing.T) { Records: []Record{{Content: `"my-acme-challenge"`}}, }} - resp, err := client.UpdateRecords(t.Context(), "example.org", rrSet) + resp, err := client.UpdateRecords(context.Background(), "example.org", rrSet) require.NoError(t, err) expected := &APIResponse{Status: "ok", Message: "RRsets updated"} diff --git a/providers/dns/rcodezero/rcodezero.go b/providers/dns/rcodezero/rcodezero.go index 010a6dadc..3011f193f 100644 --- a/providers/dns/rcodezero/rcodezero.go +++ b/providers/dns/rcodezero/rcodezero.go @@ -8,10 +8,8 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/rcodezero/internal" ) @@ -27,8 +25,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIToken string @@ -42,7 +38,7 @@ type Config struct { func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 4*time.Minute), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 240*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), @@ -87,8 +83,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{config: config, client: client}, nil } diff --git a/providers/dns/rcodezero/rcodezero.toml b/providers/dns/rcodezero/rcodezero.toml index c2a4a1e7b..a012736f4 100644 --- a/providers/dns/rcodezero/rcodezero.toml +++ b/providers/dns/rcodezero/rcodezero.toml @@ -6,7 +6,7 @@ Since = "v4.13" Example = ''' RCODEZERO_API_TOKEN= \ -lego --dns rcodezero -d '*.example.com' -d example.com run +lego --email you@example.com --dns rcodezero --domains my.example.org run ''' Additional = ''' @@ -23,10 +23,10 @@ RcodeZero is an Anycast Network so the distribution of the DNS01-Challenge can t [Configuration.Credentials] RCODEZERO_API_TOKEN = "API token" [Configuration.Additional] - RCODEZERO_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" - RCODEZERO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 240)" - RCODEZERO_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - RCODEZERO_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + RCODEZERO_POLLING_INTERVAL = "Time between DNS propagation check" + RCODEZERO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + RCODEZERO_TTL = "The TTL of the TXT record used for the DNS challenge" + RCODEZERO_HTTP_TIMEOUT = "API request timeout" [Links] # Note: the API endpoint used inside the client is not documented. diff --git a/providers/dns/rcodezero/rcodezero_test.go b/providers/dns/rcodezero/rcodezero_test.go index a4a242c30..1f0946072 100644 --- a/providers/dns/rcodezero/rcodezero_test.go +++ b/providers/dns/rcodezero/rcodezero_test.go @@ -37,7 +37,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -95,7 +94,6 @@ func TestLivePresentAndCleanup(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/regfish/regfish.go b/providers/dns/regfish/regfish.go deleted file mode 100644 index 85aac92e5..000000000 --- a/providers/dns/regfish/regfish.go +++ /dev/null @@ -1,157 +0,0 @@ -// Package regfish implements a DNS provider for solving the DNS-01 challenge using Regfish. -package regfish - -import ( - "errors" - "fmt" - "net/http" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - regfishapi "github.com/regfish/regfish-dnsapi-go" -) - -// Environment variables names. -const ( - envNamespace = "REGFISH_" - - EnvAPIKey = envNamespace + "API_KEY" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *regfishapi.Client - - recordIDs map[string]int - recordIDsMu sync.Mutex -} - -// NewDNSProvider returns a DNSProvider instance configured for Regfish. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIKey) - if err != nil { - return nil, fmt.Errorf("regfish: %w", err) - } - - config := NewDefaultConfig() - config.APIKey = values[EnvAPIKey] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Regfish. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("regfish: the configuration of the DNS provider is nil") - } - - if config.APIKey == "" { - return nil, errors.New("regfish: credentials missing") - } - - client := regfishapi.NewClient(config.APIKey) - - if config.HTTPClient != nil { - client.Client = config.HTTPClient - } else { - // Because the regfishapi.NewClient uses an empty http.Client. - client.Client = &http.Client{Timeout: 30 * time.Second} - } - - client.Client = clientdebug.Wrap(client.Client) - - return &DNSProvider{ - config: config, - client: client, - recordIDs: make(map[string]int), - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - record := regfishapi.Record{ - Name: info.EffectiveFQDN, - Type: "TXT", - Data: info.Value, - TTL: d.config.TTL, - } - - newRecord, err := d.client.CreateRecord(record) - if err != nil { - return fmt.Errorf("regfish: create record: %w", err) - } - - d.recordIDsMu.Lock() - d.recordIDs[token] = newRecord.ID - d.recordIDsMu.Unlock() - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - // get the record's unique ID from when we created it - d.recordIDsMu.Lock() - recordID, ok := d.recordIDs[token] - d.recordIDsMu.Unlock() - - if !ok { - return fmt.Errorf("regfish: unknown record ID for '%s'", info.EffectiveFQDN) - } - - err := d.client.DeleteRecord(recordID) - if err != nil { - return fmt.Errorf("regfish: delete record: %w", err) - } - - // Delete record ID from map - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/regfish/regfish.toml b/providers/dns/regfish/regfish.toml deleted file mode 100644 index fbaacbde4..000000000 --- a/providers/dns/regfish/regfish.toml +++ /dev/null @@ -1,23 +0,0 @@ -Name = "Regfish" -Description = '''''' -URL = "https://regfish.de/" -Code = "regfish" -Since = "v4.20.0" - -Example = ''' -REGFISH_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns regfish -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - REGFISH_API_KEY = "API key" - [Configuration.Additional] - REGFISH_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - REGFISH_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - REGFISH_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - REGFISH_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://regfish.readme.io/" - GoClient = "https://github.com/regfish/regfish-dnsapi-go" diff --git a/providers/dns/regfish/regfish_test.go b/providers/dns/regfish/regfish_test.go deleted file mode 100644 index 6613bd508..000000000 --- a/providers/dns/regfish/regfish_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package regfish - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAPIKey: "secret", - }, - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "regfish: some credentials information are missing: REGFISH_API_KEY", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiKey string - expected string - }{ - { - desc: "success", - apiKey: "secret", - }, - { - desc: "missing credentials", - expected: "regfish: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIKey = test.apiKey - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/regru/internal/client.go b/providers/dns/regru/internal/client.go index b0b86d567..8d91f4a66 100644 --- a/providers/dns/regru/internal/client.go +++ b/providers/dns/regru/internal/client.go @@ -38,7 +38,7 @@ func NewClient(username, password string) *Client { // RemoveTxtRecord removes a TXT record. // https://www.reg.ru/support/help/api2#zone_remove_record -func (c *Client) RemoveTxtRecord(ctx context.Context, domain, subDomain, content string) error { +func (c Client) RemoveTxtRecord(ctx context.Context, domain, subDomain, content string) error { request := RemoveRecordRequest{ Domains: []Domain{{DName: domain}}, SubDomain: subDomain, @@ -57,7 +57,7 @@ func (c *Client) RemoveTxtRecord(ctx context.Context, domain, subDomain, content // AddTXTRecord adds a TXT record. // https://www.reg.ru/support/help/api2#zone_add_txt -func (c *Client) AddTXTRecord(ctx context.Context, domain, subDomain, content string) error { +func (c Client) AddTXTRecord(ctx context.Context, domain, subDomain, content string) error { request := AddTxtRequest{ Domains: []Domain{{DName: domain}}, SubDomain: subDomain, @@ -73,17 +73,20 @@ func (c *Client) AddTXTRecord(ctx context.Context, domain, subDomain, content st return resp.HasError() } -func (c *Client) doRequest(ctx context.Context, request any, fragments ...string) (*APIResponse, error) { +func (c Client) doRequest(ctx context.Context, request any, fragments ...string) (*APIResponse, error) { endpoint := c.baseURL.JoinPath(fragments...) + query := endpoint.Query() + query.Set("username", c.username) + query.Set("password", c.password) + endpoint.RawQuery = query.Encode() + inputData, err := json.Marshal(request) if err != nil { return nil, fmt.Errorf("failed to create input data: %w", err) } data := url.Values{} - data.Set("username", c.username) - data.Set("password", c.password) data.Set("input_data", string(inputData)) data.Set("input_format", "json") @@ -111,7 +114,6 @@ func (c *Client) doRequest(ctx context.Context, request any, fragments ...string } var apiResp APIResponse - err = json.Unmarshal(raw, &apiResp) if err != nil { return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) @@ -124,7 +126,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) var errAPI APIResponse - err := json.Unmarshal(raw, &errAPI) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/regru/internal/client_test.go b/providers/dns/regru/internal/client_test.go index 002da0185..fa3f16702 100644 --- a/providers/dns/regru/internal/client_test.go +++ b/providers/dns/regru/internal/client_test.go @@ -1,60 +1,61 @@ package internal import ( - "net/http/httptest" + "context" + "net/http" "net/url" + "os" "testing" + "time" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("user", "secret") - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckHeader(). - WithContentTypeFromURLEncoded(), - ) -} +const ( + noopBaseURL = "https://api.reg.ru/api/regru2/nop" + officialTestUser = "test" + officialTestPassword = "test" +) func TestRemoveRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /zone/remove_record", - servermock.ResponseFromFixture("remove_record.json"), - servermock.CheckForm().Strict(). - With("input_data", `{"domains":[{"dname":"test.ru"}],"subdomain":"_acme-challenge","content":"txttxttxt","record_type":"TXT","output_content_type":"plain"}`). - With("username", "user"). - With("password", "secret"). - With("input_format", "json")). - Build(t) + // TODO(ldez): remove skip when the reg.ru API will be fixed. + t.Skip("there is a bug with the reg.ru API: INTERNAL_API_ERROR: Внутренняя ошибка, status code: 503") - err := client.RemoveTxtRecord(t.Context(), "test.ru", "_acme-challenge", "txttxttxt") + client := NewClient(officialTestUser, officialTestPassword) + client.HTTPClient = &http.Client{Timeout: 30 * time.Second} + + err := client.RemoveTxtRecord(context.Background(), "test.ru", "_acme-challenge", "txttxttxt") require.NoError(t, err) } func TestRemoveRecord_errors(t *testing.T) { + // TODO(ldez): remove skip when the reg.ru API will be fixed. + if os.Getenv("CI") == "true" { + t.Skip("there is a bug with the reg.ru and GitHub action: dial tcp 194.58.116.30:443: i/o timeout") + } + testCases := []struct { desc string domain string - response string + username string + password string + baseURL string expected string }{ { desc: "authentication failed", domain: "test.ru", - response: "remove_record_error_auth.json", + username: "", + password: "", + baseURL: noopBaseURL, expected: "API error: NO_AUTH: No authorization mechanism selected", }, { desc: "domain error", domain: "", - response: "remove_record_error_domain.json", + username: officialTestUser, + password: officialTestPassword, + baseURL: defaultBaseURL, expected: "API error: NO_DOMAIN: domain_name not given or empty", }, } @@ -63,48 +64,55 @@ func TestRemoveRecord_errors(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - client := mockBuilder(). - Route("POST /zone/remove_record", servermock.ResponseFromFixture(test.response)). - Build(t) + client := NewClient(test.username, test.username) + client.HTTPClient = &http.Client{Timeout: 30 * time.Second} + client.baseURL, _ = url.Parse(test.baseURL) - err := client.RemoveTxtRecord(t.Context(), test.domain, "_acme-challenge", "txttxttxt") + err := client.RemoveTxtRecord(context.Background(), test.domain, "_acme-challenge", "txttxttxt") require.EqualError(t, err, test.expected) }) } } func TestAddTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /zone/add_txt", - servermock.ResponseFromFixture("add_txt_record.json"), - servermock.CheckForm().Strict(). - With("input_data", `{"domains":[{"dname":"test.ru"}],"subdomain":"_acme-challenge","text":"txttxttxt","output_content_type":"plain"}`). - With("username", "user"). - With("password", "secret"). - With("input_format", "json")). - Build(t) + // TODO(ldez): remove skip when the reg.ru API will be fixed. + t.Skip("there is a bug with the reg.ru API: INTERNAL_API_ERROR: Внутренняя ошибка, status code: 503") - err := client.AddTXTRecord(t.Context(), "test.ru", "_acme-challenge", "txttxttxt") + client := NewClient(officialTestUser, officialTestPassword) + client.HTTPClient = &http.Client{Timeout: 30 * time.Second} + + err := client.AddTXTRecord(context.Background(), "test.ru", "_acme-challenge", "txttxttxt") require.NoError(t, err) } func TestAddTXTRecord_errors(t *testing.T) { + // TODO(ldez): remove skip when the reg.ru API will be fixed. + if os.Getenv("CI") == "true" { + t.Skip("there is a bug with the reg.ru and GitHub action: dial tcp 194.58.116.30:443: i/o timeout") + } + testCases := []struct { desc string domain string - response string + username string + password string + baseURL string expected string }{ { desc: "authentication failed", domain: "test.ru", - response: "add_txt_record_error_auth.json", + username: "", + password: "", + baseURL: noopBaseURL, expected: "API error: NO_AUTH: No authorization mechanism selected", }, { desc: "domain error", domain: "", - response: "add_txt_record_error_domain.json", + username: officialTestUser, + password: officialTestPassword, + baseURL: defaultBaseURL, expected: "API error: NO_DOMAIN: domain_name not given or empty", }, } @@ -113,11 +121,11 @@ func TestAddTXTRecord_errors(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - client := mockBuilder(). - Route("POST /zone/add_txt", servermock.ResponseFromFixture(test.response)). - Build(t) + client := NewClient(test.username, test.username) + client.HTTPClient = &http.Client{Timeout: 30 * time.Second} + client.baseURL, _ = url.Parse(test.baseURL) - err := client.AddTXTRecord(t.Context(), test.domain, "_acme-challenge", "txttxttxt") + err := client.AddTXTRecord(context.Background(), test.domain, "_acme-challenge", "txttxttxt") require.EqualError(t, err, test.expected) }) } diff --git a/providers/dns/regru/internal/fixtures/add_txt_record.json b/providers/dns/regru/internal/fixtures/add_txt_record.json deleted file mode 100644 index 06306b4c4..000000000 --- a/providers/dns/regru/internal/fixtures/add_txt_record.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "answer": { - "domains": [ - { - "dname": "test.ru", - "result": "success", - "service_id": 12345 - } - ] - }, - "charset": "utf-8", - "messagestore": null, - "result": "success" -} diff --git a/providers/dns/regru/internal/fixtures/add_txt_record_error_auth.json b/providers/dns/regru/internal/fixtures/add_txt_record_error_auth.json deleted file mode 100644 index 2d5314bf3..000000000 --- a/providers/dns/regru/internal/fixtures/add_txt_record_error_auth.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "charset": "utf-8", - "error_code": "NO_AUTH", - "error_params": { - "command_name": "nop/zone/add_txt" - }, - "error_text": "No authorization mechanism selected", - "messagestore": null, - "result": "error" -} diff --git a/providers/dns/regru/internal/fixtures/add_txt_record_error_domain.json b/providers/dns/regru/internal/fixtures/add_txt_record_error_domain.json deleted file mode 100644 index 305846ed1..000000000 --- a/providers/dns/regru/internal/fixtures/add_txt_record_error_domain.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "answer": { - "domains": [ - { - "error_code": "NO_DOMAIN", - "error_text": "domain_name not given or empty", - "result": "error" - } - ] - }, - "charset": "utf-8", - "messagestore": null, - "result": "success" -} diff --git a/providers/dns/regru/internal/fixtures/remove_record.json b/providers/dns/regru/internal/fixtures/remove_record.json deleted file mode 100644 index 06306b4c4..000000000 --- a/providers/dns/regru/internal/fixtures/remove_record.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "answer": { - "domains": [ - { - "dname": "test.ru", - "result": "success", - "service_id": 12345 - } - ] - }, - "charset": "utf-8", - "messagestore": null, - "result": "success" -} diff --git a/providers/dns/regru/internal/fixtures/remove_record_error_auth.json b/providers/dns/regru/internal/fixtures/remove_record_error_auth.json deleted file mode 100644 index 98c429c53..000000000 --- a/providers/dns/regru/internal/fixtures/remove_record_error_auth.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "charset" : "utf-8", - "error_code" : "NO_AUTH", - "error_params" : { - "command_name" : "nop/zone/remove_record" - }, - "error_text" : "No authorization mechanism selected", - "messagestore" : null, - "result" : "error" -} diff --git a/providers/dns/regru/internal/fixtures/remove_record_error_domain.json b/providers/dns/regru/internal/fixtures/remove_record_error_domain.json deleted file mode 100644 index a9ca88ff7..000000000 --- a/providers/dns/regru/internal/fixtures/remove_record_error_domain.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "answer" : { - "domains" : [ - { - "error_code" : "NO_DOMAIN", - "error_text" : "domain_name not given or empty", - "result" : "error" - } - ] - }, - "charset" : "utf-8", - "messagestore" : null, - "result" : "success" -} diff --git a/providers/dns/regru/internal/readme.md b/providers/dns/regru/internal/readme.md deleted file mode 100644 index 5f13012d2..000000000 --- a/providers/dns/regru/internal/readme.md +++ /dev/null @@ -1,6 +0,0 @@ -Test account (with the default endpoint): -- user: `test` -- password: `test` - -Noop endpoint: -- https://api.reg.ru/api/regru2/nop diff --git a/providers/dns/regru/regru.go b/providers/dns/regru/regru.go index b06b355c1..144b7faf9 100644 --- a/providers/dns/regru/regru.go +++ b/providers/dns/regru/regru.go @@ -9,10 +9,8 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/regru/internal" ) @@ -31,8 +29,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Username string @@ -98,8 +94,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - if config.TLSCert != "" || config.TLSKey != "" { if config.TLSCert == "" { return nil, errors.New("regru: TLS certificate is missing") diff --git a/providers/dns/regru/regru.toml b/providers/dns/regru/regru.toml index 728bb2bf7..5bdb2c987 100644 --- a/providers/dns/regru/regru.toml +++ b/providers/dns/regru/regru.toml @@ -7,7 +7,7 @@ Since = "v3.5.0" Example = ''' REGRU_USERNAME=xxxxxx \ REGRU_PASSWORD=yyyyyy \ -lego --dns regru -d '*.example.com' -d example.com run +lego --email you@example.com --dns regru --domains my.example.org run ''' [Configuration] @@ -17,10 +17,10 @@ lego --dns regru -d '*.example.com' -d example.com run [Configuration.Additional] REGRU_TLS_CERT = "authentication certificate" REGRU_TLS_KEY = "authentication private key" - REGRU_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - REGRU_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - REGRU_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - REGRU_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + REGRU_POLLING_INTERVAL = "Time between DNS propagation check" + REGRU_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + REGRU_TTL = "The TTL of the TXT record used for the DNS challenge" + REGRU_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.reg.ru/support/help/api2" diff --git a/providers/dns/regru/regru_test.go b/providers/dns/regru/regru_test.go index 762eeb4d3..15d86d75c 100644 --- a/providers/dns/regru/regru_test.go +++ b/providers/dns/regru/regru_test.go @@ -57,7 +57,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -130,7 +129,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -144,7 +142,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/rfc2136/internal/fixtures/invalid_field.conf b/providers/dns/rfc2136/internal/fixtures/invalid_field.conf deleted file mode 100644 index 07c6a7be2..000000000 --- a/providers/dns/rfc2136/internal/fixtures/invalid_field.conf +++ /dev/null @@ -1,4 +0,0 @@ -key "example.com" { - algorithm; - secret "TCG5A6/lOHUGbW0e/9RYYbzWDFMlj1pIxCvybLBayBg="; -}; diff --git a/providers/dns/rfc2136/internal/fixtures/invalid_key.conf b/providers/dns/rfc2136/internal/fixtures/invalid_key.conf deleted file mode 100644 index 965888eae..000000000 --- a/providers/dns/rfc2136/internal/fixtures/invalid_key.conf +++ /dev/null @@ -1,4 +0,0 @@ -key { - algorithm hmac-sha256; - secret "TCG5A6/lOHUGbW0e/9RYYbzWDFMlj1pIxCvybLBayBg="; -}; diff --git a/providers/dns/rfc2136/internal/fixtures/mising_algo.conf b/providers/dns/rfc2136/internal/fixtures/mising_algo.conf deleted file mode 100644 index 530323172..000000000 --- a/providers/dns/rfc2136/internal/fixtures/mising_algo.conf +++ /dev/null @@ -1,3 +0,0 @@ -key "example.com" { - secret "TCG5A6/lOHUGbW0e/9RYYbzWDFMlj1pIxCvybLBayBg="; -}; diff --git a/providers/dns/rfc2136/internal/fixtures/missing_secret.conf b/providers/dns/rfc2136/internal/fixtures/missing_secret.conf deleted file mode 100644 index f45eeac30..000000000 --- a/providers/dns/rfc2136/internal/fixtures/missing_secret.conf +++ /dev/null @@ -1,3 +0,0 @@ -key "example.com" { - algorithm hmac-sha256; -}; diff --git a/providers/dns/rfc2136/internal/fixtures/sample.conf b/providers/dns/rfc2136/internal/fixtures/sample.conf deleted file mode 100644 index 6e249e8a5..000000000 --- a/providers/dns/rfc2136/internal/fixtures/sample.conf +++ /dev/null @@ -1,4 +0,0 @@ -key "example.com" { - algorithm hmac-sha256; - secret "TCG5A6/lOHUGbW0e/9RYYbzWDFMlj1pIxCvybLBayBg="; -}; diff --git a/providers/dns/rfc2136/internal/fixtures/text_after.conf b/providers/dns/rfc2136/internal/fixtures/text_after.conf deleted file mode 100644 index 9b1cf8e58..000000000 --- a/providers/dns/rfc2136/internal/fixtures/text_after.conf +++ /dev/null @@ -1,9 +0,0 @@ -key "example.com" { - algorithm hmac-sha256; - secret "TCG5A6/lOHUGbW0e/9RYYbzWDFMlj1pIxCvybLBayBg="; -}; - -key "example.org" { - algorithm hmac-sha512; - secret "v6CkK3gop6HXj4+dcWiLXLGSYKVY5J1cTMjDsdl/Ah9B8aWfTgjwFBoHHyiHWSyvwWPDuEIRs2Pqm8nedca4+g=="; -}; diff --git a/providers/dns/rfc2136/internal/fixtures/text_before.conf b/providers/dns/rfc2136/internal/fixtures/text_before.conf deleted file mode 100644 index 0a8415b21..000000000 --- a/providers/dns/rfc2136/internal/fixtures/text_before.conf +++ /dev/null @@ -1,8 +0,0 @@ -foo { - bar example; -}; - -key "example.com" { - algorithm hmac-sha256; - secret "TCG5A6/lOHUGbW0e/9RYYbzWDFMlj1pIxCvybLBayBg="; -}; diff --git a/providers/dns/rfc2136/internal/readme.md b/providers/dns/rfc2136/internal/readme.md deleted file mode 100644 index d0ecae7f4..000000000 --- a/providers/dns/rfc2136/internal/readme.md +++ /dev/null @@ -1,10 +0,0 @@ -# TSIG Key File - -How to generate example: - -```console -$ docker run --rm -it -v $(pwd):/app -w /app alpine sh -/app # apk add bind -/app # tsig-keygen example.com > sample1.conf -/app # tsig-keygen -a hmac-sha512 example.com > sample2.conf -``` diff --git a/providers/dns/rfc2136/internal/tsigkey.go b/providers/dns/rfc2136/internal/tsigkey.go deleted file mode 100644 index b4672f44d..000000000 --- a/providers/dns/rfc2136/internal/tsigkey.go +++ /dev/null @@ -1,89 +0,0 @@ -package internal - -import ( - "bufio" - "fmt" - "os" - "strings" -) - -type Key struct { - Name string - Algorithm string - Secret string -} - -// ReadTSIGFile reads TSIG key file generated with `tsig-keygen`. -func ReadTSIGFile(filename string) (*Key, error) { - file, err := os.Open(filename) - if err != nil { - return nil, fmt.Errorf("open file: %w", err) - } - - defer func() { _ = file.Close() }() - - key := &Key{} - - var read bool - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := strings.TrimSpace(strings.TrimSuffix(scanner.Text(), ";")) - - if line == "" { - continue - } - - if read && line == "}" { - break - } - - fields := strings.Fields(line) - - switch { - case fields[0] == "key": - read = true - - if len(fields) != 3 { - return nil, fmt.Errorf("invalid key line: %s", line) - } - - key.Name = safeUnquote(fields[1]) - - case !read: - continue - - default: - if len(fields) != 2 { - continue - } - - v := safeUnquote(fields[1]) - - switch safeUnquote(fields[0]) { - case "algorithm": - key.Algorithm = v - case "secret": - key.Secret = v - default: - continue - } - } - } - - return key, nil -} - -func safeUnquote(v string) string { - if len(v) < 2 { - // empty or single character string - return v - } - - if v[0] == '"' && v[len(v)-1] == '"' { - // string wrapped in quotes - return v[1 : len(v)-1] - } - - return v -} diff --git a/providers/dns/rfc2136/internal/tsigkey_test.go b/providers/dns/rfc2136/internal/tsigkey_test.go deleted file mode 100644 index 4ed7f6616..000000000 --- a/providers/dns/rfc2136/internal/tsigkey_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package internal - -import ( - "path/filepath" - "runtime" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestReadTSIGFile(t *testing.T) { - testCases := []struct { - desc string - filename string - expected *Key - }{ - { - desc: "basic", - filename: "sample.conf", - expected: &Key{Name: "example.com", Algorithm: "hmac-sha256", Secret: "TCG5A6/lOHUGbW0e/9RYYbzWDFMlj1pIxCvybLBayBg="}, - }, - { - desc: "data before the key", - filename: "text_before.conf", - expected: &Key{Name: "example.com", Algorithm: "hmac-sha256", Secret: "TCG5A6/lOHUGbW0e/9RYYbzWDFMlj1pIxCvybLBayBg="}, - }, - { - desc: "data after the key", - filename: "text_after.conf", - expected: &Key{Name: "example.com", Algorithm: "hmac-sha256", Secret: "TCG5A6/lOHUGbW0e/9RYYbzWDFMlj1pIxCvybLBayBg="}, - }, - { - desc: "ignore missing secret", - filename: "missing_secret.conf", - expected: &Key{Name: "example.com", Algorithm: "hmac-sha256"}, - }, - { - desc: "ignore missing algorithm", - filename: "mising_algo.conf", - expected: &Key{Name: "example.com", Secret: "TCG5A6/lOHUGbW0e/9RYYbzWDFMlj1pIxCvybLBayBg="}, - }, - { - desc: "ignore invalid field format", - filename: "invalid_field.conf", - expected: &Key{Name: "example.com", Secret: "TCG5A6/lOHUGbW0e/9RYYbzWDFMlj1pIxCvybLBayBg="}, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - key, err := ReadTSIGFile(filepath.Join("fixtures", test.filename)) - require.NoError(t, err) - - assert.Equal(t, test.expected, key) - }) - } -} - -func TestReadTSIGFile_error(t *testing.T) { - if runtime.GOOS != "linux" { - // Because error messages are different on Windows. - t.Skip("only for UNIX systems") - } - - testCases := []struct { - desc string - filename string - expected string - }{ - { - desc: "missing file", - filename: "missing.conf", - expected: "open file: open fixtures/missing.conf: no such file or directory", - }, - { - desc: "invalid key format", - filename: "invalid_key.conf", - expected: "invalid key line: key {", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - _, err := ReadTSIGFile(filepath.Join("fixtures", test.filename)) - require.Error(t, err) - - require.EqualError(t, err, test.expected) - }) - } -} diff --git a/providers/dns/rfc2136/rfc2136.go b/providers/dns/rfc2136/rfc2136.go index 2c4fe7aeb..8a7dedc80 100644 --- a/providers/dns/rfc2136/rfc2136.go +++ b/providers/dns/rfc2136/rfc2136.go @@ -8,10 +8,8 @@ import ( "strings" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/rfc2136/internal" "github.com/miekg/dns" ) @@ -19,14 +17,11 @@ import ( const ( envNamespace = "RFC2136_" - EnvTSIGFile = envNamespace + "TSIG_FILE" - EnvTSIGKey = envNamespace + "TSIG_KEY" EnvTSIGSecret = envNamespace + "TSIG_SECRET" EnvTSIGAlgorithm = envNamespace + "TSIG_ALGORITHM" - - EnvNameserver = envNamespace + "NAMESERVER" - EnvDNSTimeout = envNamespace + "DNS_TIMEOUT" + EnvNameserver = envNamespace + "NAMESERVER" + EnvDNSTimeout = envNamespace + "DNS_TIMEOUT" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" @@ -34,18 +29,12 @@ const ( EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { - Nameserver string - - TSIGFile string - - TSIGAlgorithm string - TSIGKey string - TSIGSecret string - + Nameserver string + TSIGAlgorithm string + TSIGKey string + TSIGSecret string PropagationTimeout time.Duration PollingInterval time.Duration TTL int @@ -58,8 +47,8 @@ func NewDefaultConfig() *Config { return &Config{ TSIGAlgorithm: env.GetOrDefaultString(EnvTSIGAlgorithm, dns.HmacSHA1), TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, env.GetOrDefaultSecond("RFC2136_TIMEOUT", dns01.DefaultPropagationTimeout)), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, env.GetOrDefaultSecond("RFC2136_TIMEOUT", 60*time.Second)), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), DNSTimeout: env.GetOrDefaultSecond(EnvDNSTimeout, 10*time.Second), } @@ -87,9 +76,6 @@ func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() config.Nameserver = values[EnvNameserver] - - config.TSIGFile = env.GetOrDefaultString(EnvTSIGFile, "") - config.TSIGKey = env.GetOrFile(EnvTSIGKey) config.TSIGSecret = env.GetOrFile(EnvTSIGSecret) @@ -106,15 +92,8 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("rfc2136: nameserver missing") } - if config.TSIGFile != "" { - key, err := internal.ReadTSIGFile(config.TSIGFile) - if err != nil { - return nil, fmt.Errorf("rfc2136: read TSIG file %s: %w", config.TSIGFile, err) - } - - config.TSIGAlgorithm = key.Algorithm - config.TSIGKey = key.Name - config.TSIGSecret = key.Secret + if config.TSIGAlgorithm == "" { + config.TSIGAlgorithm = dns.HmacSHA1 } // Append the default DNS port if none is specified. @@ -129,23 +108,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config.TSIGKey == "" || config.TSIGSecret == "" { config.TSIGKey = "" config.TSIGSecret = "" - } else { - // zonename must be in canonical form (lowercase, fqdn, see RFC 4034 Section 6.2) - config.TSIGKey = dns.CanonicalName(config.TSIGKey) - } - - if config.TSIGAlgorithm == "" { - config.TSIGAlgorithm = dns.HmacSHA1 - } else { - // To be compatible with https://github.com/miekg/dns/blob/master/tsig.go - config.TSIGAlgorithm = dns.Fqdn(config.TSIGAlgorithm) - } - - switch config.TSIGAlgorithm { - case dns.HmacSHA1, dns.HmacSHA224, dns.HmacSHA256, dns.HmacSHA384, dns.HmacSHA512: - // valid algorithm - default: - return nil, fmt.Errorf("rfc2136: unsupported TSIG algorithm: %s", config.TSIGAlgorithm) } return &DNSProvider{config: config}, nil @@ -171,7 +133,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("rfc2136: failed to insert: %w", err) } - return nil } @@ -183,7 +144,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("rfc2136: failed to remove: %w", err) } - return nil } @@ -195,14 +155,14 @@ func (d *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error { } // Create RR - rrs := []dns.RR{&dns.TXT{ - Hdr: dns.RR_Header{Name: fqdn, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: uint32(ttl)}, - Txt: []string{value}, - }} + rr := new(dns.TXT) + rr.Hdr = dns.RR_Header{Name: fqdn, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: uint32(ttl)} + rr.Txt = []string{value} + rrs := []dns.RR{rr} // Create dynamic update packet - m := new(dns.Msg).SetUpdate(zone) - + m := new(dns.Msg) + m.SetUpdate(zone) switch action { case "INSERT": // Always remove old challenge left over from who knows what. @@ -219,10 +179,13 @@ func (d *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error { // TSIG authentication / msg signing if d.config.TSIGKey != "" && d.config.TSIGSecret != "" { - m.SetTsig(d.config.TSIGKey, d.config.TSIGAlgorithm, 300, time.Now().Unix()) + key := strings.ToLower(dns.Fqdn(d.config.TSIGKey)) + alg := dns.Fqdn(d.config.TSIGAlgorithm) + m.SetTsig(key, alg, 300, time.Now().Unix()) - // Secret(s) for TSIG map[]. - c.TsigSecret = map[string]string{d.config.TSIGKey: d.config.TSIGSecret} + // secret(s) for Tsig map[], + // zonename must be in canonical form (lowercase, fqdn, see RFC 4034 Section 6.2) + c.TsigSecret = map[string]string{key: d.config.TSIGSecret} } // Send the query @@ -230,7 +193,6 @@ func (d *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error { if err != nil { return fmt.Errorf("DNS update failed: %w", err) } - if reply != nil && reply.Rcode != dns.RcodeSuccess { return fmt.Errorf("DNS update failed: server replied: %s", dns.RcodeToString[reply.Rcode]) } diff --git a/providers/dns/rfc2136/rfc2136.toml b/providers/dns/rfc2136/rfc2136.toml index 6b5bbe599..4125aa557 100644 --- a/providers/dns/rfc2136/rfc2136.toml +++ b/providers/dns/rfc2136/rfc2136.toml @@ -6,33 +6,34 @@ Since = "v0.3.0" Example = ''' RFC2136_NAMESERVER=127.0.0.1 \ -RFC2136_TSIG_KEY=example.com \ +RFC2136_TSIG_KEY=lego \ RFC2136_TSIG_ALGORITHM=hmac-sha256. \ RFC2136_TSIG_SECRET=YWJjZGVmZGdoaWprbG1ub3BxcnN0dXZ3eHl6MTIzNDU= \ -lego --dns rfc2136 -d '*.example.com' -d example.com run +lego --email you@example.com --dns rfc2136 --domains my.example.org run ## --- -keyname=example.com; keyfile=example.com.key; tsig-keygen $keyname > $keyfile +keyname=lego; keyfile=lego.key; tsig-keygen $keyname > $keyfile RFC2136_NAMESERVER=127.0.0.1 \ -RFC2136_TSIG_FILE="$keyfile" \ -lego --dns rfc2136 -d '*.example.com' -d example.com run +RFC2136_TSIG_KEY="$keyname" \ +RFC2136_TSIG_ALGORITHM="$( awk -F'[ ";]' '/algorithm/ { print $2 }' $keyfile )." \ +RFC2136_TSIG_SECRET="$( awk -F'[ ";]' '/secret/ { print $3 }' $keyfile )" \ +lego --email you@example.com --dns rfc2136 --domains my.example.org run ''' [Configuration] [Configuration.Credentials] - RFC2136_TSIG_KEY = "Name of the secret key as defined in DNS server configuration. To disable TSIG authentication, leave the `RFC2136_TSIG_KEY` variable unset." - RFC2136_TSIG_SECRET = "Secret key payload. To disable TSIG authentication, leave the `RFC2136_TSIG_SECRET` variable unset." - RFC2136_TSIG_ALGORITHM = "TSIG algorithm. See [miekg/dns#tsig.go](https://github.com/miekg/dns/blob/master/tsig.go) for supported values. To disable TSIG authentication, leave the `RFC2136_TSIG_KEY` or `RFC2136_TSIG_SECRET` variables unset." + RFC2136_TSIG_KEY = "Name of the secret key as defined in DNS server configuration. To disable TSIG authentication, leave the `RFC2136_TSIG*` variables unset." + RFC2136_TSIG_SECRET = "Secret key payload. To disable TSIG authentication, leave the` RFC2136_TSIG*` variables unset." + RFC2136_TSIG_ALGORITHM = "TSIG algorithm. See [miekg/dns#tsig.go](https://github.com/miekg/dns/blob/master/tsig.go) for supported values. To disable TSIG authentication, leave the `RFC2136_TSIG*` variables unset." RFC2136_NAMESERVER = 'Network address in the form "host" or "host:port"' [Configuration.Additional] - RFC2136_TSIG_FILE = "Path to a key file generated by tsig-keygen" - RFC2136_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - RFC2136_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - RFC2136_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - RFC2136_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 60)" - RFC2136_DNS_TIMEOUT = "API request timeout in seconds (Default: 10)" + RFC2136_POLLING_INTERVAL = "Time between DNS propagation check" + RFC2136_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + RFC2136_TTL = "The TTL of the TXT record used for the DNS challenge" + RFC2136_DNS_TIMEOUT = "API request timeout" + RFC2136_SEQUENCE_INTERVAL = "Time between sequential requests" [Links] API = "https://www.rfc-editor.org/rfc/rfc2136.html" diff --git a/providers/dns/rfc2136/rfc2136_test.go b/providers/dns/rfc2136/rfc2136_test.go index ce4859e84..235ce4e4e 100644 --- a/providers/dns/rfc2136/rfc2136_test.go +++ b/providers/dns/rfc2136/rfc2136_test.go @@ -2,21 +2,23 @@ package rfc2136 import ( "bytes" + "fmt" + "net" "strings" + "sync" "testing" "time" "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/dnsmock" "github.com/miekg/dns" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( fakeDomain = "123456789.www.example.com" fakeKeyAuth = "123d==" - fakeValue = "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY" + fakeValue = "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo" fakeFqdn = "_acme-challenge.123456789.www.example.com." fakeZone = "example.com." fakeTTL = 120 @@ -24,153 +26,39 @@ const ( fakeTsigSecret = "IwBTJx9wrDp4Y1RyC3H0gA==" ) -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest( - EnvTSIGFile, - EnvTSIGKey, - EnvTSIGSecret, - EnvTSIGAlgorithm, - EnvNameserver, - EnvDNSTimeout, -).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvNameserver: "example.com", - }, - }, - { - desc: "missing nameserver", - envVars: map[string]string{ - EnvNameserver: "", - }, - expected: "rfc2136: some credentials information are missing: RFC2136_NAMESERVER", - }, - { - desc: "invalid algorithm", - envVars: map[string]string{ - EnvNameserver: "example.com", - EnvTSIGKey: "", - EnvTSIGSecret: "", - EnvTSIGAlgorithm: "foo", - }, - expected: "rfc2136: unsupported TSIG algorithm: foo.", - }, - { - desc: "valid TSIG file", - envVars: map[string]string{ - EnvNameserver: "example.com", - EnvTSIGFile: "./internal/fixtures/sample.conf", - }, - }, - { - desc: "invalid TSIG file", - envVars: map[string]string{ - EnvNameserver: "example.com", - EnvTSIGFile: "./internal/fixtures/invalid_key.conf", - }, - expected: "rfc2136: read TSIG file ./internal/fixtures/invalid_key.conf: invalid key line: key {", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - expected string - nameserver string - tsigFile string - tsigAlgorithm string - tsigKey string - tsigSecret string - }{ - { - desc: "success", - nameserver: "example.com", - }, - { - desc: "missing nameserver", - expected: "rfc2136: nameserver missing", - }, - { - desc: "invalid algorithm", - nameserver: "example.com", - tsigAlgorithm: "foo", - expected: "rfc2136: unsupported TSIG algorithm: foo.", - }, - { - desc: "valid TSIG file", - nameserver: "example.com", - tsigFile: "./internal/fixtures/sample.conf", - }, - { - desc: "invalid TSIG file", - nameserver: "example.com", - tsigFile: "./internal/fixtures/invalid_key.conf", - expected: "rfc2136: read TSIG file ./internal/fixtures/invalid_key.conf: invalid key line: key {", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.Nameserver = test.nameserver - config.TSIGFile = test.tsigFile - config.TSIGAlgorithm = test.tsigAlgorithm - config.TSIGKey = test.tsigKey - config.TSIGSecret = test.tsigSecret - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestDNSProvider_Present_success(t *testing.T) { +func TestCanaryLocalTestServer(t *testing.T) { dns01.ClearFqdnCache() + dns.HandleFunc("example.com.", serverHandlerHello) + defer dns.HandleRemove("example.com.") - addr := dnsmock.NewServer(). - Query(fakeZone+" SOA", dnsmock.SOA("")). - Update(fakeZone+" SOA", dnsmock.Noop). - Build(t) + server, addr, err := runLocalDNSTestServer(false) + require.NoError(t, err, "Failed to start test server") + defer func() { _ = server.Shutdown() }() + + c := new(dns.Client) + m := new(dns.Msg) + + m.SetQuestion("example.com.", dns.TypeTXT) + + r, _, err := c.Exchange(m, addr) + require.NoError(t, err, "Failed to communicate with test server") + assert.Len(t, r.Extra, 1, "Failed to communicate with test server") + + txt := r.Extra[0].(*dns.TXT).Txt[0] + assert.Equal(t, "Hello world", txt) +} + +func TestServerSuccess(t *testing.T) { + dns01.ClearFqdnCache() + dns.HandleFunc(fakeZone, serverHandlerReturnSuccess) + defer dns.HandleRemove(fakeZone) + + server, addr, err := runLocalDNSTestServer(false) + require.NoError(t, err, "Failed to start test server") + defer func() { _ = server.Shutdown() }() config := NewDefaultConfig() - config.Nameserver = addr.String() + config.Nameserver = addr provider, err := NewDNSProviderConfig(config) require.NoError(t, err) @@ -179,98 +67,39 @@ func TestDNSProvider_Present_success(t *testing.T) { require.NoError(t, err) } -func TestDNSProvider_Present_success_updatePacket(t *testing.T) { +func TestServerError(t *testing.T) { dns01.ClearFqdnCache() + dns.HandleFunc(fakeZone, serverHandlerReturnErr) + defer dns.HandleRemove(fakeZone) - reqChan := make(chan *dns.Msg, 1) - - addr := dnsmock.NewServer(). - Query("_acme-challenge.123456789.www.example.com. SOA", dnsmock.SOA(fakeZone)). - Update(fakeZone+" SOA", func(w dns.ResponseWriter, req *dns.Msg) { - dnsmock.Noop(w, req) - - // Only talk back when it is not the SOA RR. - reqChan <- req - }). - Build(t) + server, addr, err := runLocalDNSTestServer(false) + require.NoError(t, err, "Failed to start test server") + defer func() { _ = server.Shutdown() }() config := NewDefaultConfig() - config.Nameserver = addr.String() - - provider, err := NewDNSProviderConfig(config) - require.NoError(t, err) - - err = provider.Present(fakeDomain, "", fakeKeyAuth) - require.NoError(t, err) - - select { - case <-time.After(time.Second): - t.Fatal("timeout waiting for request") - - case rcvMsg := <-reqChan: - txtRR := &dns.TXT{ - Hdr: dns.RR_Header{Name: fakeFqdn, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: fakeTTL}, - Txt: []string{fakeValue}, - } - - m := new(dns.Msg).SetUpdate(fakeZone) - - m.RemoveRRset([]dns.RR{txtRR}) - m.Insert([]dns.RR{txtRR}) - - expected, err := m.Pack() - require.NoError(t, err, "error packing") - - rcvMsg.Id = m.Id - - actual, err := rcvMsg.Pack() - require.NoError(t, err, "error packing") - - if !bytes.Equal(actual, expected) { - tmp := new(dns.Msg) - require.NoError(t, tmp.Unpack(actual)) - - t.Errorf("Expected msg:\n%s", m) - t.Errorf("Actual msg:\n%s", tmp) - } - } -} - -func TestDNSProvider_Present_error(t *testing.T) { - dns01.ClearFqdnCache() - - addr := dnsmock.NewServer(). - Query(fakeZone+" SOA", dnsmock.Error(dns.RcodeNotZone)). - Build(t) - - config := NewDefaultConfig() - config.Nameserver = addr.String() + config.Nameserver = addr provider, err := NewDNSProviderConfig(config) require.NoError(t, err) err = provider.Present(fakeDomain, "", fakeKeyAuth) require.Error(t, err) - if !strings.Contains(err.Error(), "NOTZONE") { t.Errorf("Expected Present() to return an error with the 'NOTZONE' rcode string, but it did not: %v", err) } } -func TestDNSProvider_Present_tsig_success(t *testing.T) { +func TestTsigClient(t *testing.T) { dns01.ClearFqdnCache() + dns.HandleFunc(fakeZone, serverHandlerReturnSuccess) + defer dns.HandleRemove(fakeZone) - addr := dnsmock.NewServer(). - Query(fakeZone+" SOA", dnsmock.SOA("")). - Update(fakeZone+" SOA", handleTSIG). - Build(t, func(server *dns.Server) error { - server.TsigSecret = map[string]string{fakeTsigKey: fakeTsigSecret} - - return nil - }) + server, addr, err := runLocalDNSTestServer(true) + require.NoError(t, err, "Failed to start test server") + defer func() { _ = server.Shutdown() }() config := NewDefaultConfig() - config.Nameserver = addr.String() + config.Nameserver = addr config.TSIGKey = fakeTsigKey config.TSIGSecret = fakeTsigSecret @@ -281,50 +110,143 @@ func TestDNSProvider_Present_tsig_success(t *testing.T) { require.NoError(t, err) } -func TestDNSProvider_Present_tsig_error(t *testing.T) { +func TestValidUpdatePacket(t *testing.T) { + reqChan := make(chan *dns.Msg, 10) + dns01.ClearFqdnCache() + dns.HandleFunc(fakeZone, serverHandlerPassBackRequest(reqChan)) + defer dns.HandleRemove(fakeZone) - addr := dnsmock.NewServer(). - Query(fakeZone+" SOA", dnsmock.SOA("")). - Update(fakeZone+" SOA", handleTSIG). - Build(t, func(server *dns.Server) error { - server.TsigSecret = map[string]string{"example.org": fakeTsigSecret} + server, addr, err := runLocalDNSTestServer(false) + require.NoError(t, err, "Failed to start test server") + defer func() { _ = server.Shutdown() }() - return nil - }) - - config := NewDefaultConfig() - config.Nameserver = addr.String() - config.TSIGKey = fakeTsigKey - config.TSIGSecret = fakeTsigSecret - - provider, err := NewDNSProviderConfig(config) - require.NoError(t, err) - - err = provider.Present(fakeDomain, "", fakeKeyAuth) - require.Error(t, err) - require.EqualError(t, err, "rfc2136: failed to insert: DNS update failed: server replied: NOTZONE") -} - -func handleTSIG(w dns.ResponseWriter, req *dns.Msg) { + txtRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN TXT %s", fakeFqdn, fakeTTL, fakeValue)) + rrs := []dns.RR{txtRR} m := new(dns.Msg) + m.SetUpdate(fakeZone) + m.RemoveRRset(rrs) + m.Insert(rrs) + expectStr := m.String() - tsig := req.IsTsig() - if tsig == nil { - _ = w.WriteMsg(m.SetRcode(req, dns.RcodeRefused)) - return + expect, err := m.Pack() + require.NoError(t, err, "error packing") + + config := NewDefaultConfig() + config.Nameserver = addr + + provider, err := NewDNSProviderConfig(config) + require.NoError(t, err) + + err = provider.Present(fakeDomain, "", "1234d==") + require.NoError(t, err) + + rcvMsg := <-reqChan + rcvMsg.Id = m.Id + + actual, err := rcvMsg.Pack() + require.NoError(t, err, "error packing") + + if !bytes.Equal(actual, expect) { + tmp := new(dns.Msg) + if err := tmp.Unpack(actual); err != nil { + t.Fatalf("Error unpacking actual msg: %v", err) + } + t.Errorf("Expected msg:\n%s", expectStr) + t.Errorf("Actual msg:\n%v", tmp) + } +} + +func runLocalDNSTestServer(tsig bool) (*dns.Server, string, error) { + pc, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + return nil, "", err + } + + server := &dns.Server{ + PacketConn: pc, + ReadTimeout: time.Hour, + WriteTimeout: time.Hour, + MsgAcceptFunc: func(dh dns.Header) dns.MsgAcceptAction { + // bypass defaultMsgAcceptFunc to allow dynamic update (https://github.com/miekg/dns/pull/830) + return dns.MsgAccept + }, + } + + if tsig { + server.TsigSecret = map[string]string{fakeTsigKey: fakeTsigSecret} + } + + waitLock := sync.Mutex{} + waitLock.Lock() + server.NotifyStartedFunc = waitLock.Unlock + + go func() { + _ = server.ActivateAndServe() + pc.Close() + }() + + waitLock.Lock() + return server, pc.LocalAddr().String(), nil +} + +func serverHandlerHello(w dns.ResponseWriter, req *dns.Msg) { + m := new(dns.Msg) + m.SetReply(req) + m.Extra = make([]dns.RR, 1) + m.Extra[0] = &dns.TXT{ + Hdr: dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 0}, + Txt: []string{"Hello world"}, + } + _ = w.WriteMsg(m) +} + +func serverHandlerReturnSuccess(w dns.ResponseWriter, req *dns.Msg) { + m := new(dns.Msg) + m.SetReply(req) + if req.Opcode == dns.OpcodeQuery && req.Question[0].Qtype == dns.TypeSOA && req.Question[0].Qclass == dns.ClassINET { + // Return SOA to appease findZoneByFqdn() + soaRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN SOA ns1.%s admin.%s 2016022801 28800 7200 2419200 1200", fakeZone, fakeTTL, fakeZone, fakeZone)) + m.Answer = []dns.RR{soaRR} + } + + if t := req.IsTsig(); t != nil { + if w.TsigStatus() == nil { + // Validated + m.SetTsig(fakeZone, dns.HmacSHA1, 300, time.Now().Unix()) + } + } + + _ = w.WriteMsg(m) +} + +func serverHandlerReturnErr(w dns.ResponseWriter, req *dns.Msg) { + m := new(dns.Msg) + m.SetRcode(req, dns.RcodeNotZone) + _ = w.WriteMsg(m) +} + +func serverHandlerPassBackRequest(reqChan chan *dns.Msg) func(w dns.ResponseWriter, req *dns.Msg) { + return func(w dns.ResponseWriter, req *dns.Msg) { + m := new(dns.Msg) + m.SetReply(req) + if req.Opcode == dns.OpcodeQuery && req.Question[0].Qtype == dns.TypeSOA && req.Question[0].Qclass == dns.ClassINET { + // Return SOA to appease findZoneByFqdn() + soaRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN SOA ns1.%s admin.%s 2016022801 28800 7200 2419200 1200", fakeZone, fakeTTL, fakeZone, fakeZone)) + m.Answer = []dns.RR{soaRR} + } + + if t := req.IsTsig(); t != nil { + if w.TsigStatus() == nil { + // Validated + m.SetTsig(fakeZone, dns.HmacSHA1, 300, time.Now().Unix()) + } + } + + _ = w.WriteMsg(m) + if req.Opcode != dns.OpcodeQuery || req.Question[0].Qtype != dns.TypeSOA || req.Question[0].Qclass != dns.ClassINET { + // Only talk back when it is not the SOA RR. + reqChan <- req + } } - - err := w.TsigStatus() - if err != nil { - _ = w.WriteMsg(m.SetRcode(req, dns.RcodeNotZone)) - - return - } - - // Validated - _ = w.WriteMsg(m. - SetReply(req). - SetTsig(tsig.Hdr.Name, tsig.Algorithm, tsig.Fudge, time.Now().Unix()), - ) } diff --git a/providers/dns/rimuhosting/rimuhosting.go b/providers/dns/rimuhosting/rimuhosting.go index 7a7e99f60..09b31d4f5 100644 --- a/providers/dns/rimuhosting/rimuhosting.go +++ b/providers/dns/rimuhosting/rimuhosting.go @@ -2,12 +2,12 @@ package rimuhosting import ( + "context" "errors" "fmt" "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/internal/rimuhosting" @@ -25,15 +25,20 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. -type Config = rimuhosting.Config +type Config struct { + APIKey string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, rimuhosting.DefaultTTL), + TTL: env.GetOrDefaultInt(EnvTTL, 3600), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ @@ -44,7 +49,8 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - prv challenge.ProviderTimeout + config *Config + client *rimuhosting.Client } // NewDNSProvider returns a DNSProvider instance configured for RimuHosting. @@ -67,19 +73,48 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("rimuhosting: the configuration of the DNS provider is nil") } - provider, err := rimuhosting.NewDNSProviderConfig(config, "") - if err != nil { - return nil, fmt.Errorf("rimuhosting: %w", err) + if config.APIKey == "" { + return nil, errors.New("rimuhosting: incomplete credentials, missing API key") } - return &DNSProvider{prv: provider}, nil + client := rimuhosting.NewClient(config.APIKey) + client.BaseURL = rimuhosting.DefaultRimuHostingBaseURL + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + return &DNSProvider{config: config, client: client}, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - err := d.prv.Present(domain, token, keyAuth) + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + records, err := d.client.FindTXTRecords(ctx, dns01.UnFqdn(info.EffectiveFQDN)) if err != nil { - return fmt.Errorf("rimuhosting: %w", err) + return fmt.Errorf("rimuhosting: failed to find record(s) for %s: %w", domain, err) + } + + actions := []rimuhosting.ActionParameter{ + rimuhosting.NewAddRecordAction(dns01.UnFqdn(info.EffectiveFQDN), info.Value, d.config.TTL), + } + + for _, record := range records { + actions = append(actions, rimuhosting.NewAddRecordAction(record.Name, record.Content, d.config.TTL)) + } + + _, err = d.client.DoActions(ctx, actions...) + if err != nil { + return fmt.Errorf("rimuhosting: failed to add record(s) for %s: %w", domain, err) } return nil @@ -87,16 +122,14 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - err := d.prv.CleanUp(domain, token, keyAuth) + info := dns01.GetChallengeInfo(domain, keyAuth) + + action := rimuhosting.NewDeleteRecordAction(dns01.UnFqdn(info.EffectiveFQDN), info.Value) + + _, err := d.client.DoActions(context.Background(), action) if err != nil { - return fmt.Errorf("rimuhosting: %w", err) + return fmt.Errorf("rimuhosting: failed to delete record for %s: %w", domain, err) } return nil } - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.prv.Timeout() -} diff --git a/providers/dns/rimuhosting/rimuhosting.toml b/providers/dns/rimuhosting/rimuhosting.toml index c1994e2cc..fc5ee5826 100644 --- a/providers/dns/rimuhosting/rimuhosting.toml +++ b/providers/dns/rimuhosting/rimuhosting.toml @@ -6,17 +6,17 @@ Since = "v0.3.5" Example = ''' RIMUHOSTING_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --dns rimuhosting -d '*.example.com' -d example.com run +lego --email you@example.com --dns rimuhosting --domains my.example.org run ''' [Configuration] [Configuration.Credentials] RIMUHOSTING_API_KEY = "User API key" [Configuration.Additional] - RIMUHOSTING_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - RIMUHOSTING_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - RIMUHOSTING_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)" - RIMUHOSTING_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + RIMUHOSTING_POLLING_INTERVAL = "Time between DNS propagation check" + RIMUHOSTING_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + RIMUHOSTING_TTL = "The TTL of the TXT record used for the DNS challenge" + RIMUHOSTING_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://rimuhosting.com/dns/dyndns.jsp" diff --git a/providers/dns/rimuhosting/rimuhosting_test.go b/providers/dns/rimuhosting/rimuhosting_test.go index 878ec14da..cbdacedc4 100644 --- a/providers/dns/rimuhosting/rimuhosting_test.go +++ b/providers/dns/rimuhosting/rimuhosting_test.go @@ -36,7 +36,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -46,7 +45,7 @@ func TestNewDNSProvider(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - require.NotNil(t, p.prv) + require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } @@ -84,7 +83,7 @@ func TestNewDNSProviderConfig(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - require.NotNil(t, p.prv) + require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } @@ -98,7 +97,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -112,7 +110,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/route53/fixtures/changeResourceRecordSetsResponse.xml b/providers/dns/route53/fixtures/changeResourceRecordSetsResponse.xml deleted file mode 100644 index 68dba580f..000000000 --- a/providers/dns/route53/fixtures/changeResourceRecordSetsResponse.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - /change/123456 - PENDING - 2016-02-10T01:36:41.958Z - - diff --git a/providers/dns/route53/fixtures/getChangeResponse.xml b/providers/dns/route53/fixtures/getChangeResponse.xml deleted file mode 100644 index f22c09460..000000000 --- a/providers/dns/route53/fixtures/getChangeResponse.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - 123456 - INSYNC - 2016-02-10T01:36:41.958Z - - diff --git a/providers/dns/route53/fixtures/listHostedZonesByNameResponse.xml b/providers/dns/route53/fixtures/listHostedZonesByNameResponse.xml deleted file mode 100644 index db47ba1e1..000000000 --- a/providers/dns/route53/fixtures/listHostedZonesByNameResponse.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - /hostedzone/ABCDEFG - example.com. - D2224C5B-684A-DB4A-BB9A-E09E3BAFEA7A - - Test comment - false - - 10 - - - true - example2.com - ZLT12321321124 - 1 - diff --git a/providers/dns/route53/fixtures_test.go b/providers/dns/route53/fixtures_test.go new file mode 100644 index 000000000..444a88003 --- /dev/null +++ b/providers/dns/route53/fixtures_test.go @@ -0,0 +1,39 @@ +package route53 + +const ChangeResourceRecordSetsResponse = ` + + + /change/123456 + PENDING + 2016-02-10T01:36:41.958Z + +` + +const ListHostedZonesByNameResponse = ` + + + + /hostedzone/ABCDEFG + example.com. + D2224C5B-684A-DB4A-BB9A-E09E3BAFEA7A + + Test comment + false + + 10 + + + true + example2.com + ZLT12321321124 + 1 +` + +const GetChangeResponse = ` + + + 123456 + INSYNC + 2016-02-10T01:36:41.958Z + +` diff --git a/providers/dns/route53/mock_test.go b/providers/dns/route53/mock_test.go new file mode 100644 index 000000000..022767385 --- /dev/null +++ b/providers/dns/route53/mock_test.go @@ -0,0 +1,51 @@ +package route53 + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// MockResponse represents a predefined response used by a mock server. +type MockResponse struct { + StatusCode int + Body string +} + +// MockResponseMap maps request paths to responses. +type MockResponseMap map[string]MockResponse + +func setupTest(t *testing.T, responses MockResponseMap) string { + t.Helper() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + resp, ok := responses[path] + if !ok { + resp, ok = responses[r.RequestURI] + if !ok { + msg := fmt.Sprintf("Requested path not found in response map: %s", path) + require.FailNow(t, msg) + } + } + + w.Header().Set("Content-Type", "application/xml") + w.WriteHeader(resp.StatusCode) + _, err := w.Write([]byte(resp.Body)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + server := httptest.NewServer(handler) + t.Cleanup(server.Close) + + time.Sleep(100 * time.Millisecond) + + return server.URL +} diff --git a/providers/dns/route53/route53.go b/providers/dns/route53/route53.go index b41c95dac..394aa506d 100644 --- a/providers/dns/route53/route53.go +++ b/providers/dns/route53/route53.go @@ -17,12 +17,9 @@ import ( "github.com/aws/aws-sdk-go-v2/service/route53" awstypes "github.com/aws/aws-sdk-go-v2/service/route53/types" "github.com/aws/aws-sdk-go-v2/service/sts" - "github.com/cenkalti/backoff/v5" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/platform/wait" - "github.com/go-acme/lego/v4/providers/dns/internal/ptr" ) // Environment variables names. @@ -36,7 +33,6 @@ const ( EnvMaxRetries = envNamespace + "MAX_RETRIES" EnvAssumeRoleArn = envNamespace + "ASSUME_ROLE_ARN" EnvExternalID = envNamespace + "EXTERNAL_ID" - EnvPrivateZone = envNamespace + "PRIVATE_ZONE" EnvWaitForRecordSetsChanged = envNamespace + "WAIT_FOR_RECORD_SETS_CHANGED" @@ -45,8 +41,6 @@ const ( EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { // Static credential chain. @@ -60,7 +54,6 @@ type Config struct { MaxRetries int AssumeRoleArn string ExternalID string - PrivateZone bool WaitForRecordSetsChanged bool @@ -78,7 +71,6 @@ func NewDefaultConfig() *Config { MaxRetries: env.GetOrDefaultInt(EnvMaxRetries, 5), AssumeRoleArn: env.GetOrDefaultString(EnvAssumeRoleArn, ""), ExternalID: env.GetOrDefaultString(EnvExternalID, ""), - PrivateZone: env.GetOrDefaultBool(EnvPrivateZone, false), WaitForRecordSetsChanged: env.GetOrDefaultBool(EnvWaitForRecordSetsChanged, true), @@ -155,9 +147,8 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { realValue := `"` + info.Value + `"` var found bool - for _, record := range records { - if ptr.Deref(record.Value) == realValue { + if deref(record.Value) == realValue { found = true } } @@ -201,9 +192,8 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } var nonLegoRecords []awstypes.ResourceRecord - for _, record := range existingRecords { - if ptr.Deref(record.Value) != `"`+info.Value+`"` { + if deref(record.Value) != `"`+info.Value+`"` { nonLegoRecords = append(nonLegoRecords, record) } } @@ -252,22 +242,18 @@ func (d *DNSProvider) changeRecord(ctx context.Context, action awstypes.ChangeAc changeID := resp.ChangeInfo.Id if d.config.WaitForRecordSetsChanged { - return wait.Retry(ctx, - func() error { - resp, err := d.client.GetChange(ctx, &route53.GetChangeInput{Id: changeID}) - if err != nil { - return fmt.Errorf("failed to query change status: %w", err) - } + return wait.For("route53", d.config.PropagationTimeout, d.config.PollingInterval, func() (bool, error) { + resp, err := d.client.GetChange(ctx, &route53.GetChangeInput{Id: changeID}) + if err != nil { + return false, fmt.Errorf("failed to query change status: %w", err) + } - if resp.ChangeInfo.Status != awstypes.ChangeStatusInsync { - return fmt.Errorf("unable to retrieve change: ID=%s, status=%s", ptr.Deref(changeID), resp.ChangeInfo.Status) - } + if resp.ChangeInfo.Status == awstypes.ChangeStatusInsync { + return true, nil + } - return nil - }, - backoff.WithBackOff(backoff.NewConstantBackOff(d.config.PollingInterval)), - backoff.WithMaxElapsedTime(d.config.PropagationTimeout), - ) + return false, fmt.Errorf("unable to retrieve change: ID=%s", deref(changeID)) + }) } return nil @@ -292,7 +278,7 @@ func (d *DNSProvider) getExistingRecordSets(ctx context.Context, hostedZoneID, f var records []awstypes.ResourceRecord for _, recordSet := range recordSetsOutput.ResourceRecordSets { - if ptr.Deref(recordSet.Name) == fqdn { + if deref(recordSet.Name) == fqdn { records = append(records, recordSet.ResourceRecords...) } } @@ -314,18 +300,16 @@ func (d *DNSProvider) getHostedZoneID(ctx context.Context, fqdn string) (string, reqParams := &route53.ListHostedZonesByNameInput{ DNSName: aws.String(dns01.UnFqdn(authZone)), } - resp, err := d.client.ListHostedZonesByName(ctx, reqParams) if err != nil { return "", err } var hostedZoneID string - for _, hostedZone := range resp.HostedZones { // .Name has a trailing dot - if ptr.Deref(hostedZone.Name) == authZone && d.config.PrivateZone == hostedZone.Config.PrivateZone { - hostedZoneID = ptr.Deref(hostedZone.Id) + if !hostedZone.Config.PrivateZone && deref(hostedZone.Name) == authZone { + hostedZoneID = deref(hostedZone.Id) break } } @@ -354,10 +338,12 @@ func createAWSConfig(ctx context.Context, config *Config) (aws.Config, error) { // causing a high number of consecutive throttling errors. // For reference: Route 53 enforces an account-wide(!) 5req/s query limit. options.Backoff = retry.BackoffDelayerFunc(func(attempt int, err error) (time.Duration, error) { - retryCount := min(attempt, 7) + retryCount := attempt + if retryCount > 7 { + retryCount = 7 + } delay := (1 << uint(retryCount)) * (rand.Intn(50) + 200) - return time.Duration(delay) * time.Millisecond, nil }) }) @@ -405,3 +391,12 @@ func createAWSConfigCheckParams(config *Config) error { return nil } + +func deref[T string | int | int32 | int64 | bool](v *T) T { + if v == nil { + var zero T + return zero + } + + return *v +} diff --git a/providers/dns/route53/route53.toml b/providers/dns/route53/route53.toml index 607d9ef31..da8b489a3 100644 --- a/providers/dns/route53/route53.toml +++ b/providers/dns/route53/route53.toml @@ -9,7 +9,7 @@ AWS_ACCESS_KEY_ID=your_key_id \ AWS_SECRET_ACCESS_KEY=your_secret_access_key \ AWS_REGION=aws-region \ AWS_HOSTED_ZONE_ID=your_hosted_zone_id \ -lego --dns route53 -d '*.example.com' -d example.com run +lego --domains example.com --email your_example@email.com --dns route53 --accept-tos=true run ''' Additional = ''' @@ -133,12 +133,11 @@ Replace `Z11111112222222333333` with your hosted zone ID and `example.com` with AWS_EXTERNAL_ID = "Managed by STS AssumeRole API operation (`AWS_EXTERNAL_ID_FILE` is not supported)" AWS_WAIT_FOR_RECORD_SETS_CHANGED = "Wait for changes to be INSYNC (it can be unstable)" [Configuration.Additional] - AWS_PRIVATE_ZONE = "Set to true to use private zones only (default: use public zones only)" AWS_SHARED_CREDENTIALS_FILE = "Managed by the AWS client. Shared credentials file." AWS_MAX_RETRIES = "The number of maximum returns the service will use to make an individual API request" - AWS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 4)" - AWS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - AWS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 10)" + AWS_POLLING_INTERVAL = "Time between DNS propagation check" + AWS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + AWS_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://docs.aws.amazon.com/Route53/latest/APIReference/API_Operations_Amazon_Route_53.html" diff --git a/providers/dns/route53/route53_integration_test.go b/providers/dns/route53/route53_integration_test.go index b80294013..2fbcf5206 100644 --- a/providers/dns/route53/route53_integration_test.go +++ b/providers/dns/route53/route53_integration_test.go @@ -1,12 +1,12 @@ package route53 import ( + "context" "testing" "github.com/aws/aws-sdk-go-v2/aws" awsconfig "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/route53" - "github.com/go-acme/lego/v4/providers/dns/internal/ptr" "github.com/stretchr/testify/require" ) @@ -28,7 +28,7 @@ func TestLiveTTL(t *testing.T) { // we need a separate R53 client here as the one in the DNS provider is unexported. fqdn := "_acme-challenge." + domain + "." - ctx := t.Context() + ctx := context.Background() cfg, err := awsconfig.LoadDefaultConfig(ctx) require.NoError(t, err) @@ -42,7 +42,7 @@ func TestLiveTTL(t *testing.T) { } }() - zoneID, err := provider.getHostedZoneID(t.Context(), fqdn) + zoneID, err := provider.getHostedZoneID(context.Background(), fqdn) require.NoError(t, err) params := &route53.ListResourceRecordSetsInput{ @@ -52,7 +52,7 @@ func TestLiveTTL(t *testing.T) { require.NoError(t, err) for _, v := range resp.ResourceRecordSets { - if ptr.Deref(v.Name) == fqdn && v.Type == "TXT" && ptr.Deref(v.TTL) == 10 { + if deref(v.Name) == fqdn && v.Type == "TXT" && deref(v.TTL) == 10 { return } } diff --git a/providers/dns/route53/route53_test.go b/providers/dns/route53/route53_test.go index 41ed824bc..4301ca10f 100644 --- a/providers/dns/route53/route53_test.go +++ b/providers/dns/route53/route53_test.go @@ -1,7 +1,7 @@ package route53 import ( - "net/http/httptest" + "context" "os" "testing" "time" @@ -11,7 +11,6 @@ import ( "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/route53" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -24,7 +23,6 @@ var envTest = tester.NewEnvTest( EnvRegion, EnvHostedZoneID, EnvMaxRetries, - EnvPrivateZone, EnvTTL, EnvPropagationTimeout, EnvPollingInterval, @@ -32,16 +30,39 @@ var envTest = tester.NewEnvTest( WithDomain(envDomain). WithLiveTestRequirements(EnvAccessKeyID, EnvSecretAccessKey, EnvRegion, envDomain) +type endpointResolverMock struct { + endpoint string +} + +func (e endpointResolverMock) ResolveEndpoint(_, _ string, _ ...interface{}) (aws.Endpoint, error) { + return aws.Endpoint{URL: e.endpoint}, nil +} + +func makeTestProvider(t *testing.T, serverURL string) *DNSProvider { + t.Helper() + + cfg := aws.Config{ + Credentials: credentials.NewStaticCredentialsProvider("abc", "123", " "), + Region: "mock-region", + EndpointResolverWithOptions: endpointResolverMock{endpoint: serverURL}, + RetryMaxAttempts: 1, + } + + return &DNSProvider{ + client: route53.NewFromConfig(cfg), + config: NewDefaultConfig(), + } +} + func Test_loadCredentials_FromEnv(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() _ = os.Setenv(EnvAccessKeyID, "123") _ = os.Setenv(EnvSecretAccessKey, "456") _ = os.Setenv(EnvRegion, "us-east-1") - ctx := t.Context() + ctx := context.Background() cfg, err := awsconfig.LoadDefaultConfig(ctx) require.NoError(t, err) @@ -61,12 +82,11 @@ func Test_loadCredentials_FromEnv(t *testing.T) { func Test_loadRegion_FromEnv(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() _ = os.Setenv(EnvRegion, "foo") - cfg, err := awsconfig.LoadDefaultConfig(t.Context()) + cfg, err := awsconfig.LoadDefaultConfig(context.Background()) require.NoError(t, err) assert.Equal(t, "foo", cfg.Region, "Region") @@ -74,7 +94,6 @@ func Test_loadRegion_FromEnv(t *testing.T) { func Test_getHostedZoneID_FromEnv(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() expectedZoneID := "zoneID" @@ -84,8 +103,8 @@ func Test_getHostedZoneID_FromEnv(t *testing.T) { provider, err := NewDNSProvider() require.NoError(t, err) - hostedZoneID, err := provider.getHostedZoneID(t.Context(), "whatever") - require.NoError(t, err) + hostedZoneID, err := provider.getHostedZoneID(context.Background(), "whatever") + require.NoError(t, err, "HostedZoneID") assert.Equal(t, expectedZoneID, hostedZoneID) } @@ -131,7 +150,6 @@ func TestNewDefaultConfig(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { envTest.ClearEnv() - for key, value := range test.envVars { _ = os.Setenv(key, value) } @@ -144,50 +162,27 @@ func TestNewDefaultConfig(t *testing.T) { } func TestDNSProvider_Present(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - provider := servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - cfg := aws.Config{ - HTTPClient: server.Client(), - Credentials: credentials.NewStaticCredentialsProvider("abc", "123", " "), - Region: "mock-region", - BaseEndpoint: aws.String(server.URL), - RetryMaxAttempts: 1, - } - - return &DNSProvider{ - client: route53.NewFromConfig(cfg), - config: NewDefaultConfig(), - }, nil + mockResponses := MockResponseMap{ + "/2013-04-01/hostedzonesbyname": {StatusCode: 200, Body: ListHostedZonesByNameResponse}, + "/2013-04-01/hostedzone/ABCDEFG/rrset": {StatusCode: 200, Body: ChangeResourceRecordSetsResponse}, + "/2013-04-01/change/123456": {StatusCode: 200, Body: GetChangeResponse}, + "/2013-04-01/hostedzone/ABCDEFG/rrset?name=_acme-challenge.example.com.&type=TXT": { + StatusCode: 200, + Body: "", }, - ). - Route("GET /2013-04-01/hostedzonesbyname", - servermock.ResponseFromFixture("listHostedZonesByNameResponse.xml"). - WithHeader("Content-Type", "application/xml"), - servermock.CheckQueryParameter().Strict(). - With("dnsname", "example.com")). - Route("POST /2013-04-01/hostedzone/ABCDEFG/rrset", - servermock.ResponseFromFixture("changeResourceRecordSetsResponse.xml"). - WithHeader("Content-Type", "application/xml")). - Route("GET /2013-04-01/change/123456", - servermock.ResponseFromFixture("getChangeResponse.xml"). - WithHeader("Content-Type", "application/xml")). - Route("GET /2013-04-01/hostedzone/ABCDEFG/rrset", - servermock.Noop(). - WithHeader("Content-Type", "application/xml"), - servermock.CheckQueryParameter().Strict(). - With("name", "_acme-challenge.example.com."). - With("type", "TXT")). - Build(t) + } + + serverURL := setupTest(t, mockResponses) + + defer envTest.RestoreEnv() + envTest.ClearEnv() + provider := makeTestProvider(t, serverURL) domain := "example.com" keyAuth := "123456d==" err := provider.Present(domain, "", keyAuth) - require.NoError(t, err) + require.NoError(t, err, "Expected Present to return no error") } func Test_createAWSConfig(t *testing.T) { @@ -276,12 +271,11 @@ func Test_createAWSConfig(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.env) - ctx := t.Context() + ctx := context.Background() cfg, err := createAWSConfig(ctx, test.config) requireErr(t, err, test.wantErr) diff --git a/providers/dns/safedns/internal/client.go b/providers/dns/safedns/internal/client.go index 628618032..3e6f99919 100644 --- a/providers/dns/safedns/internal/client.go +++ b/providers/dns/safedns/internal/client.go @@ -19,7 +19,7 @@ const defaultBaseURL = "https://api.ukfast.io/safedns/v1" const authorizationHeader = "Authorization" -// Client the ANS SafeDNS client. +// Client the UKFast SafeDNS client. type Client struct { authToken string @@ -48,7 +48,6 @@ func (c *Client) AddRecord(ctx context.Context, zone string, record Record) (*Ad } respData := &AddRecordResponse{} - err = c.do(req, respData) if err != nil { return nil, fmt.Errorf("add record: %w", err) @@ -133,7 +132,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) var errAPI APIError - err := json.Unmarshal(raw, &errAPI) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/safedns/internal/client_test.go b/providers/dns/safedns/internal/client_test.go index 161a9f078..6709277cd 100644 --- a/providers/dns/safedns/internal/client_test.go +++ b/providers/dns/safedns/internal/client_test.go @@ -1,37 +1,75 @@ package internal import ( + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "strings" "testing" "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("secret") - client.baseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(), - ) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient("secret") + client.baseURL, _ = url.Parse(server.URL) + + return client, mux } func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /zones/example.com/records", - servermock.ResponseFromFixture("add_record.json"). - WithStatusCode(http.StatusCreated), - servermock.CheckRequestJSONBodyFromFixture("add_record-request.json")). - Build(t) + client, mux := setupTest(t) + + mux.HandleFunc("/zones/example.com/records", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + if req.Header.Get(authorizationHeader) != "secret" { + http.Error(rw, `{"message":"Unauthenticated"}`, http.StatusUnauthorized) + return + } + + reqBody, err := io.ReadAll(req.Body) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + expectedReqBody := `{"name":"_acme-challenge.example.com","type":"TXT","content":"\"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\"","ttl":120}` + if strings.TrimSpace(string(reqBody)) != expectedReqBody { + http.Error(rw, `{"message":"invalid request"}`, http.StatusBadRequest) + return + } + + resp := `{ + "data": { + "id": 1234567 + }, + "meta": { + "location": "https://api.ukfast.io/safedns/v1/zones/example.com/records/1234567" + } + }` + + rw.WriteHeader(http.StatusCreated) + _, err = fmt.Fprint(rw, resp) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) record := Record{ Name: "_acme-challenge.example.com", @@ -40,7 +78,7 @@ func TestClient_AddRecord(t *testing.T) { TTL: dns01.DefaultTTL, } - response, err := client.AddRecord(t.Context(), "example.com", record) + response, err := client.AddRecord(context.Background(), "example.com", record) require.NoError(t, err) expected := &AddRecordResponse{ @@ -59,42 +97,23 @@ func TestClient_AddRecord(t *testing.T) { assert.Equal(t, expected, response) } -func TestClient_AddRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /zones/example.com/records", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) - - record := Record{ - Name: "_acme-challenge.example.com", - Type: "TXT", - Content: `"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI"`, - TTL: dns01.DefaultTTL, - } - - _, err := client.AddRecord(t.Context(), "example.com", record) - require.EqualError(t, err, "add record: [status code: 401] Unauthenticated") -} - func TestClient_RemoveRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /zones/example.com/records/1234567", - servermock.Noop(). - WithStatusCode(http.StatusNoContent)). - Build(t) + client, mux := setupTest(t) - err := client.RemoveRecord(t.Context(), "example.com", 1234567) + mux.HandleFunc("/zones/example.com/records/1234567", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodDelete { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + if req.Header.Get(authorizationHeader) != "secret" { + http.Error(rw, `{"message":"Unauthenticated"}`, http.StatusUnauthorized) + return + } + + rw.WriteHeader(http.StatusNoContent) + }) + + err := client.RemoveRecord(context.Background(), "example.com", 1234567) require.NoError(t, err) } - -func TestClient_RemoveRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /zones/example.com/records/1234567", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) - - err := client.RemoveRecord(t.Context(), "example.com", 1234567) - require.EqualError(t, err, "remove record: [status code: 401] Unauthenticated") -} diff --git a/providers/dns/safedns/internal/fixtures/add_record-request.json b/providers/dns/safedns/internal/fixtures/add_record-request.json deleted file mode 100644 index 71c8813f2..000000000 --- a/providers/dns/safedns/internal/fixtures/add_record-request.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "_acme-challenge.example.com", - "type": "TXT", - "content": "\"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\"", - "ttl": 120 -} diff --git a/providers/dns/safedns/internal/fixtures/add_record.json b/providers/dns/safedns/internal/fixtures/add_record.json deleted file mode 100644 index f3c4ad883..000000000 --- a/providers/dns/safedns/internal/fixtures/add_record.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "data": { - "id": 1234567 - }, - "meta": { - "location": "https://api.ukfast.io/safedns/v1/zones/example.com/records/1234567" - } -} diff --git a/providers/dns/safedns/internal/fixtures/error.json b/providers/dns/safedns/internal/fixtures/error.json deleted file mode 100644 index 47fb5916c..000000000 --- a/providers/dns/safedns/internal/fixtures/error.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "message": "Unauthenticated" -} diff --git a/providers/dns/safedns/safedns.go b/providers/dns/safedns/safedns.go index 154cfc5ee..cbf217029 100644 --- a/providers/dns/safedns/safedns.go +++ b/providers/dns/safedns/safedns.go @@ -1,4 +1,4 @@ -// Package safedns implements a DNS provider for solving the DNS-01 challenge using ANS SafeDNS. +// Package safedns implements a DNS provider for solving the DNS-01 challenge using UKFast SafeDNS. package safedns import ( @@ -9,12 +9,9 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/safedns/internal" - "github.com/miekg/dns" ) // Environment variables. @@ -29,8 +26,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { AuthToken string @@ -75,7 +70,7 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderConfig return a DNSProvider instance configured for ANS SafeDNS. +// NewDNSProviderConfig return a DNSProvider instance configured for UKFast SafeDNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("safedns: supplied configuration was nil") @@ -91,8 +86,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, client: client, @@ -110,7 +103,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) - zone, err := dns01.FindZoneByFqdn(dns.Fqdn(info.EffectiveFQDN)) + zone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(info.EffectiveFQDN)) if err != nil { return fmt.Errorf("safedns: could not find zone for domain %q: %w", domain, err) } @@ -146,7 +139,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() - if !ok { return fmt.Errorf("safedns: unknown record ID for '%s'", info.EffectiveFQDN) } diff --git a/providers/dns/safedns/safedns.toml b/providers/dns/safedns/safedns.toml index f387f2535..b92e4630f 100644 --- a/providers/dns/safedns/safedns.toml +++ b/providers/dns/safedns/safedns.toml @@ -1,22 +1,22 @@ -Name = "ANS SafeDNS" +Name = "UKFast SafeDNS" Description = '''''' -URL = "https://www.ans.co.uk/" +URL = "https://www.ukfast.co.uk/dns-hosting.html" Code = "safedns" Since = "v4.6.0" Example = ''' SAFEDNS_AUTH_TOKEN=xxxxxx \ -lego --dns safedns -d '*.example.com' -d example.com run +lego --email you@example.com --dns safedns --domains my.example.org run ''' [Configuration] [Configuration.Credentials] SAFEDNS_AUTH_TOKEN = "Authentication token" [Configuration.Additional] - SAFEDNS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - SAFEDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - SAFEDNS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - SAFEDNS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + SAFEDNS_POLLING_INTERVAL = "Time between DNS propagation check" + SAFEDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + SAFEDNS_TTL = "The TTL of the TXT record used for the DNS challenge" + SAFEDNS_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://developers.ukfast.io/documentation/safedns" diff --git a/providers/dns/safedns/safedns_test.go b/providers/dns/safedns/safedns_test.go index ce7568056..dcb374718 100644 --- a/providers/dns/safedns/safedns_test.go +++ b/providers/dns/safedns/safedns_test.go @@ -36,7 +36,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -96,7 +95,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -110,7 +108,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/sakuracloud/sakuracloud.go b/providers/dns/sakuracloud/sakuracloud.go index 1adbe3a88..f0c8dc922 100644 --- a/providers/dns/sakuracloud/sakuracloud.go +++ b/providers/dns/sakuracloud/sakuracloud.go @@ -2,21 +2,15 @@ package sakuracloud import ( - "context" "errors" "fmt" "net/http" - "strings" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" client "github.com/sacloud/api-client-go" "github.com/sacloud/iaas-api-go" - "github.com/sacloud/iaas-api-go/defaults" "github.com/sacloud/iaas-api-go/helper/api" ) @@ -33,8 +27,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Token string @@ -102,13 +94,13 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { Options: &client.Options{ AccessToken: config.Token, AccessTokenSecret: config.Secret, - HttpClient: clientdebug.Wrap(config.HTTPClient), - UserAgent: fmt.Sprintf("%s %s", iaas.DefaultUserAgent, useragent.Get()), + HttpClient: config.HTTPClient, + UserAgent: fmt.Sprintf("go-acme/lego %s", iaas.DefaultUserAgent), }, } return &DNSProvider{ - client: iaas.NewDNSOp(newCallerWithOptions(api.MergeOptions(defaultOption, options))), + client: iaas.NewDNSOp(api.NewCallerWithOptions(api.MergeOptions(defaultOption, options))), config: config, }, nil } @@ -117,7 +109,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) - err := d.addTXTRecord(context.Background(), info.EffectiveFQDN, info.Value, d.config.TTL) + err := d.addTXTRecord(info.EffectiveFQDN, info.Value, d.config.TTL) if err != nil { return fmt.Errorf("sakuracloud: %w", err) } @@ -129,7 +121,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) - err := d.cleanupTXTRecord(context.Background(), info.EffectiveFQDN, info.Value) + err := d.cleanupTXTRecord(info.EffectiveFQDN, info.Value) if err != nil { return fmt.Errorf("sakuracloud: %w", err) } @@ -142,38 +134,3 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } - -// Extracted from https://github.com/sacloud/iaas-api-go/blob/af06b3ccc2c38625d2dc684ad39590d0ae13eed3/helper/api/caller.go#L36-L81 -// Trace and fake are removed. -// Related to https://github.com/sacloud/iaas-api-go/issues/376. -func newCallerWithOptions(opts *api.CallerOptions) iaas.APICaller { - return newCaller(opts) -} - -func newCaller(opts *api.CallerOptions) iaas.APICaller { - if opts.UserAgent == "" { - opts.UserAgent = iaas.DefaultUserAgent - } - - caller := iaas.NewClientWithOptions(opts.Options) - - defaults.DefaultStatePollingTimeout = 72 * time.Hour - - if opts.DefaultZone != "" { - iaas.APIDefaultZone = opts.DefaultZone - } - - if len(opts.Zones) > 0 { - iaas.SakuraCloudZones = opts.Zones - } - - if opts.APIRootURL != "" { - if strings.HasSuffix(opts.APIRootURL, "/") { - opts.APIRootURL = strings.TrimRight(opts.APIRootURL, "/") - } - - iaas.SakuraCloudAPIRoot = opts.APIRootURL - } - - return caller -} diff --git a/providers/dns/sakuracloud/sakuracloud.toml b/providers/dns/sakuracloud/sakuracloud.toml index a197cd27c..c6a2eeb90 100644 --- a/providers/dns/sakuracloud/sakuracloud.toml +++ b/providers/dns/sakuracloud/sakuracloud.toml @@ -7,7 +7,7 @@ Since = "v1.1.0" Example = ''' SAKURACLOUD_ACCESS_TOKEN=xxxxx \ SAKURACLOUD_ACCESS_TOKEN_SECRET=yyyyy \ -lego --dns sakuracloud -d '*.example.com' -d example.com run +lego --email you@example.com --dns sakuracloud --domains my.example.org run ''' [Configuration] @@ -15,10 +15,10 @@ lego --dns sakuracloud -d '*.example.com' -d example.com run SAKURACLOUD_ACCESS_TOKEN = "Access token" SAKURACLOUD_ACCESS_TOKEN_SECRET = "Access token secret" [Configuration.Additional] - SAKURACLOUD_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - SAKURACLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - SAKURACLOUD_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - SAKURACLOUD_HTTP_TIMEOUT = "API request timeout in seconds (Default: 10)" + SAKURACLOUD_POLLING_INTERVAL = "Time between DNS propagation check" + SAKURACLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + SAKURACLOUD_TTL = "The TTL of the TXT record used for the DNS challenge" + SAKURACLOUD_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://developer.sakura.ad.jp/cloud/api/1.1/" diff --git a/providers/dns/sakuracloud/sakuracloud_test.go b/providers/dns/sakuracloud/sakuracloud_test.go index 789a27544..93cf20ea1 100644 --- a/providers/dns/sakuracloud/sakuracloud_test.go +++ b/providers/dns/sakuracloud/sakuracloud_test.go @@ -57,7 +57,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -130,7 +129,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -144,7 +142,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/sakuracloud/wrapper.go b/providers/dns/sakuracloud/wrapper.go index ff0b78e09..a74478f6c 100644 --- a/providers/dns/sakuracloud/wrapper.go +++ b/providers/dns/sakuracloud/wrapper.go @@ -14,11 +14,11 @@ import ( // see: https://github.com/go-acme/lego/pull/850 var mu sync.Mutex -func (d *DNSProvider) addTXTRecord(ctx context.Context, fqdn, value string, ttl int) error { +func (d *DNSProvider) addTXTRecord(fqdn, value string, ttl int) error { mu.Lock() defer mu.Unlock() - zone, err := d.getHostedZone(ctx, fqdn) + zone, err := d.getHostedZone(fqdn) if err != nil { return err } @@ -35,7 +35,7 @@ func (d *DNSProvider) addTXTRecord(ctx context.Context, fqdn, value string, ttl TTL: ttl, }) - _, err = d.client.UpdateSettings(ctx, zone.ID, &iaas.DNSUpdateSettingsRequest{ + _, err = d.client.UpdateSettings(context.Background(), zone.ID, &iaas.DNSUpdateSettingsRequest{ Records: records, SettingsHash: zone.SettingsHash, }) @@ -46,11 +46,11 @@ func (d *DNSProvider) addTXTRecord(ctx context.Context, fqdn, value string, ttl return nil } -func (d *DNSProvider) cleanupTXTRecord(ctx context.Context, fqdn, value string) error { +func (d *DNSProvider) cleanupTXTRecord(fqdn, value string) error { mu.Lock() defer mu.Unlock() - zone, err := d.getHostedZone(ctx, fqdn) + zone, err := d.getHostedZone(fqdn) if err != nil { return err } @@ -61,9 +61,8 @@ func (d *DNSProvider) cleanupTXTRecord(ctx context.Context, fqdn, value string) } var updRecords iaas.DNSRecords - for _, r := range zone.Records { - if !(r.Name == subDomain && r.Type == "TXT" && r.RData == value) { //nolint:staticcheck // Clearer without De Morgan's law. + if !(r.Name == subDomain && r.Type == "TXT" && r.RData == value) { updRecords = append(updRecords, r) } } @@ -72,8 +71,7 @@ func (d *DNSProvider) cleanupTXTRecord(ctx context.Context, fqdn, value string) Records: updRecords, SettingsHash: zone.SettingsHash, } - - _, err = d.client.UpdateSettings(ctx, zone.ID, settings) + _, err = d.client.UpdateSettings(context.Background(), zone.ID, settings) if err != nil { return fmt.Errorf("API call failed: %w", err) } @@ -81,7 +79,7 @@ func (d *DNSProvider) cleanupTXTRecord(ctx context.Context, fqdn, value string) return nil } -func (d *DNSProvider) getHostedZone(ctx context.Context, domain string) (*iaas.DNS, error) { +func (d *DNSProvider) getHostedZone(domain string) (*iaas.DNS, error) { authZone, err := dns01.FindZoneByFqdn(domain) if err != nil { return nil, fmt.Errorf("could not find zone: %w", err) @@ -95,7 +93,7 @@ func (d *DNSProvider) getHostedZone(ctx context.Context, domain string) (*iaas.D }, } - res, err := d.client.Find(ctx, conditions) + res, err := d.client.Find(context.Background(), conditions) if err != nil { if iaas.IsNotFoundError(err) { return nil, fmt.Errorf("zone %s not found on SakuraCloud DNS: %w", zoneName, err) diff --git a/providers/dns/sakuracloud/wrapper_test.go b/providers/dns/sakuracloud/wrapper_test.go index 7432c67a6..91cd3ce0a 100644 --- a/providers/dns/sakuracloud/wrapper_test.go +++ b/providers/dns/sakuracloud/wrapper_test.go @@ -1,6 +1,7 @@ package sakuracloud import ( + "context" "fmt" "sync" "testing" @@ -32,7 +33,7 @@ func fakeCaller() iaas.APICaller { func createDummyZone(t *testing.T, caller iaas.APICaller) { t.Helper() - ctx := t.Context() + ctx := context.Background() dnsOp := iaas.NewDNSOp(caller) @@ -44,13 +45,12 @@ func createDummyZone(t *testing.T, caller iaas.APICaller) { if zone.Name == "example.com" { err = dnsOp.Delete(ctx, zone.ID) require.NoError(t, err) - break } } // create dummy zone - _, err = iaas.NewDNSOp(caller).Create(t.Context(), &iaas.DNSCreateRequest{Name: "example.com"}) + _, err = iaas.NewDNSOp(caller).Create(context.Background(), &iaas.DNSCreateRequest{Name: "example.com"}) require.NoError(t, err) } @@ -65,12 +65,10 @@ func TestDNSProvider_addAndCleanupRecords(t *testing.T) { require.NoError(t, err) t.Run("addTXTRecord", func(t *testing.T) { - ctx := t.Context() - - err = p.addTXTRecord(ctx, "test.example.com.", "dummyValue", 10) + err = p.addTXTRecord("test.example.com.", "dummyValue", 10) require.NoError(t, err) - updZone, e := p.getHostedZone(ctx, "test.example.com.") + updZone, e := p.getHostedZone("test.example.com.") require.NoError(t, e) require.NotNil(t, updZone) @@ -78,12 +76,10 @@ func TestDNSProvider_addAndCleanupRecords(t *testing.T) { }) t.Run("cleanupTXTRecord", func(t *testing.T) { - ctx := t.Context() - - err = p.cleanupTXTRecord(ctx, "test.example.com.", "dummyValue") + err = p.cleanupTXTRecord("test.example.com.", "dummyValue") require.NoError(t, err) - updZone, e := p.getHostedZone(ctx, "test.example.com.") + updZone, e := p.getHostedZone("test.example.com.") require.NoError(t, e) require.NotNil(t, updZone) @@ -97,7 +93,6 @@ func TestDNSProvider_concurrentAddAndCleanupRecords(t *testing.T) { dummyRecordCount := 10 var providers []*DNSProvider - for range dummyRecordCount { config := NewDefaultConfig() config.Token = "token3" @@ -114,11 +109,9 @@ func TestDNSProvider_concurrentAddAndCleanupRecords(t *testing.T) { t.Run("addTXTRecord", func(t *testing.T) { wg.Add(len(providers)) - ctx := t.Context() - for i, p := range providers { go func(j int, client *DNSProvider) { - err := client.addTXTRecord(ctx, fmt.Sprintf("test%d.example.com.", j), "dummyValue", 10) + err := client.addTXTRecord(fmt.Sprintf("test%d.example.com.", j), "dummyValue", 10) require.NoError(t, err) wg.Done() }(i, p) @@ -126,7 +119,7 @@ func TestDNSProvider_concurrentAddAndCleanupRecords(t *testing.T) { wg.Wait() - updZone, err := providers[0].getHostedZone(ctx, "example.com.") + updZone, err := providers[0].getHostedZone("example.com.") require.NoError(t, err) require.NotNil(t, updZone) @@ -136,11 +129,9 @@ func TestDNSProvider_concurrentAddAndCleanupRecords(t *testing.T) { t.Run("cleanupTXTRecord", func(t *testing.T) { wg.Add(len(providers)) - ctx := t.Context() - for i, p := range providers { go func(i int, client *DNSProvider) { - err := client.cleanupTXTRecord(ctx, fmt.Sprintf("test%d.example.com.", i), "dummyValue") + err := client.cleanupTXTRecord(fmt.Sprintf("test%d.example.com.", i), "dummyValue") require.NoError(t, err) wg.Done() }(i, p) @@ -148,7 +139,7 @@ func TestDNSProvider_concurrentAddAndCleanupRecords(t *testing.T) { wg.Wait() - updZone, err := providers[0].getHostedZone(ctx, "example.com.") + updZone, err := providers[0].getHostedZone("example.com.") require.NoError(t, err) require.NotNil(t, updZone) diff --git a/providers/dns/scaleway/scaleway.go b/providers/dns/scaleway/scaleway.go index 9d08f93b9..70b8a8ea9 100644 --- a/providers/dns/scaleway/scaleway.go +++ b/providers/dns/scaleway/scaleway.go @@ -5,20 +5,25 @@ package scaleway import ( "errors" "fmt" - "net/http" "strconv" "strings" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" scwdomain "github.com/scaleway/scaleway-sdk-go/api/domain/v2beta1" "github.com/scaleway/scaleway-sdk-go/scw" ) +const ( + minTTL = 60 + defaultPollingInterval = 10 * time.Second + defaultPropagationTimeout = 120 * time.Second +) + +// The access key is not used by the Scaleway client. +const dumpAccessKey = "SCWXXXXXXXXXXXXXXXXX" + // Environment variables names. const ( envNamespace = "SCALEWAY_" @@ -34,30 +39,16 @@ const ( EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const ( - minTTL = 60 - defaultPollingInterval = 10 * time.Second - defaultPropagationTimeout = 120 * time.Second -) - -// The access key is not used by the Scaleway client. -const dumpAccessKey = "SCWXXXXXXXXXXXXXXXXX" - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { - ProjectID string - Token string // TODO(ldez) rename to SecretKey in the next major. - AccessKey string - + ProjectID string + Token string // TODO(ldez) rename to SecretKey in the next major. + AccessKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int - HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. @@ -67,9 +58,6 @@ func NewDefaultConfig() *Config { TTL: env.GetOneWithFallback(EnvTTL, minTTL, strconv.Atoi, altEnvName(EnvTTL)), PropagationTimeout: env.GetOneWithFallback(EnvPropagationTimeout, defaultPropagationTimeout, env.ParseSecond, altEnvName(EnvPropagationTimeout)), PollingInterval: env.GetOneWithFallback(EnvPollingInterval, defaultPollingInterval, env.ParseSecond, altEnvName(EnvPollingInterval)), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, } } @@ -112,11 +100,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { configuration := []scw.ClientOption{ scw.WithAuth(config.AccessKey, config.Token), - scw.WithUserAgent(useragent.Get()), - } - - if config.HTTPClient != nil { - configuration = append(configuration, scw.WithHTTPClient(clientdebug.Wrap(config.HTTPClient))) + scw.WithUserAgent("Scaleway Lego's provider"), } if config.ProjectID != "" { diff --git a/providers/dns/scaleway/scaleway.toml b/providers/dns/scaleway/scaleway.toml index 8b556e8b1..569c032f9 100644 --- a/providers/dns/scaleway/scaleway.toml +++ b/providers/dns/scaleway/scaleway.toml @@ -6,7 +6,7 @@ Since = "v3.4.0" Example = ''' SCW_SECRET_KEY=xxxxxxx-xxxxx-xxxx-xxx-xxxxxx \ -lego --dns scaleway -d '*.example.com' -d example.com run +lego --email you@example.com --dns scaleway --domains my.example.org run ''' [Configuration] @@ -15,10 +15,9 @@ lego --dns scaleway -d '*.example.com' -d example.com run SCW_PROJECT_ID = "Project to use (optional)" [Configuration.Additional] SCW_ACCESS_KEY = "Access key" - SCW_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" - SCW_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - SCW_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" - SCW_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + SCW_POLLING_INTERVAL = "Time between DNS propagation check" + SCW_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + SCW_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://developers.scaleway.com/en/products/domain/dns/api/" diff --git a/providers/dns/scaleway/scaleway_test.go b/providers/dns/scaleway/scaleway_test.go index b683d751a..bf950e84e 100644 --- a/providers/dns/scaleway/scaleway_test.go +++ b/providers/dns/scaleway/scaleway_test.go @@ -41,7 +41,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -106,7 +105,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -120,7 +118,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/selectel/selectel.go b/providers/dns/selectel/selectel.go index 63ddd81ac..933115c7f 100644 --- a/providers/dns/selectel/selectel.go +++ b/providers/dns/selectel/selectel.go @@ -4,17 +4,20 @@ package selectel import ( + "context" "errors" "fmt" "net/http" + "net/url" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/internal/selectel" ) +const minTTL = 60 + // Environment variables names. const ( envNamespace = "SELECTEL_" @@ -28,18 +31,23 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. -type Config = selectel.Config +type Config struct { + BaseURL string + Token string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - BaseURL: env.GetOrDefaultString(EnvBaseURL, ""), - TTL: env.GetOrDefaultInt(EnvTTL, selectel.MinTTL), + BaseURL: env.GetOrDefaultString(EnvBaseURL, selectel.DefaultSelectelBaseURL), + TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, @@ -48,7 +56,8 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - prv challenge.ProviderTimeout + config *Config + client *selectel.Client } // NewDNSProvider returns a DNSProvider instance configured for Selectel Domains API. @@ -71,36 +80,89 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("selectel: the configuration of the DNS provider is nil") } - provider, err := selectel.NewDNSProviderConfig(config) + if config.Token == "" { + return nil, errors.New("selectel: credentials missing") + } + + if config.TTL < minTTL { + return nil, fmt.Errorf("selectel: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) + } + + client := selectel.NewClient(config.Token) + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + var err error + client.BaseURL, err = url.Parse(config.BaseURL) if err != nil { return nil, fmt.Errorf("selectel: %w", err) } - return &DNSProvider{prv: provider}, nil + return &DNSProvider{config: config, client: client}, nil } -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - err := d.prv.Present(domain, token, keyAuth) - if err != nil { - return fmt.Errorf("selectel: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - err := d.prv.CleanUp(domain, token, keyAuth) - if err != nil { - return fmt.Errorf("selectel: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Timeout returns the Timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.prv.Timeout() + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record to fulfill DNS-01 challenge. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + // TODO(ldez) replace domain by FQDN to follow CNAME. + domainObj, err := d.client.GetDomainByName(ctx, domain) + if err != nil { + return fmt.Errorf("selectel: %w", err) + } + + txtRecord := selectel.Record{ + Type: "TXT", + TTL: d.config.TTL, + Name: info.EffectiveFQDN, + Content: info.Value, + } + _, err = d.client.AddRecord(ctx, domainObj.ID, txtRecord) + if err != nil { + return fmt.Errorf("selectel: %w", err) + } + + return nil +} + +// CleanUp removes a TXT record used for DNS-01 challenge. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + recordName := dns01.UnFqdn(info.EffectiveFQDN) + + ctx := context.Background() + + // TODO(ldez) replace domain by FQDN to follow CNAME. + domainObj, err := d.client.GetDomainByName(ctx, domain) + if err != nil { + return fmt.Errorf("selectel: %w", err) + } + + records, err := d.client.ListRecords(ctx, domainObj.ID) + if err != nil { + return fmt.Errorf("selectel: %w", err) + } + + // Delete records with specific FQDN + var lastErr error + for _, record := range records { + if record.Name == recordName { + err = d.client.DeleteRecord(ctx, domainObj.ID, record.ID) + if err != nil { + lastErr = fmt.Errorf("selectel: %w", err) + } + } + } + + return lastErr } diff --git a/providers/dns/selectel/selectel.toml b/providers/dns/selectel/selectel.toml index 087c97b5b..2b00ee6a9 100644 --- a/providers/dns/selectel/selectel.toml +++ b/providers/dns/selectel/selectel.toml @@ -6,7 +6,7 @@ Since = "v1.2.0" Example = ''' SELECTEL_API_TOKEN=xxxxx \ -lego --dns selectel -d '*.example.com' -d example.com run +lego --email you@example.com --dns selectel --domains my.example.org run ''' [Configuration] @@ -14,10 +14,10 @@ lego --dns selectel -d '*.example.com' -d example.com run SELECTEL_API_TOKEN = "API token" [Configuration.Additional] SELECTEL_BASE_URL = "API endpoint URL" - SELECTEL_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - SELECTEL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - SELECTEL_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" - SELECTEL_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + SELECTEL_POLLING_INTERVAL = "Time between DNS propagation check" + SELECTEL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + SELECTEL_TTL = "The TTL of the TXT record used for the DNS challenge" + SELECTEL_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://kb.selectel.com/23136054.html" diff --git a/providers/dns/selectel/selectel_test.go b/providers/dns/selectel/selectel_test.go index a456f1358..0e2de2dbe 100644 --- a/providers/dns/selectel/selectel_test.go +++ b/providers/dns/selectel/selectel_test.go @@ -6,7 +6,6 @@ import ( "time" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/providers/dns/internal/selectel" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -37,7 +36,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -47,7 +45,8 @@ func TestNewDNSProvider(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - assert.NotNil(t, p.prv) + assert.NotNil(t, p.config) + assert.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } @@ -77,7 +76,7 @@ func TestNewDNSProviderConfig(t *testing.T) { desc: "bad TTL value", token: "123", ttl: 59, - expected: fmt.Sprintf("selectel: invalid TTL, TTL (59) must be greater than %d", selectel.MinTTL), + expected: fmt.Sprintf("selectel: invalid TTL, TTL (59) must be greater than %d", minTTL), }, } @@ -92,7 +91,8 @@ func TestNewDNSProviderConfig(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - assert.NotNil(t, p.prv) + assert.NotNil(t, p.config) + assert.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } @@ -106,7 +106,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -120,7 +119,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/selectelv2/selectelv2.go b/providers/dns/selectelv2/selectelv2.go index 1fcb48583..a6f1d7305 100644 --- a/providers/dns/selectelv2/selectelv2.go +++ b/providers/dns/selectelv2/selectelv2.go @@ -11,25 +11,29 @@ import ( "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" - "github.com/miekg/dns" + "github.com/go-acme/lego/v4/providers/dns/internal/selectel" selectelapi "github.com/selectel/domains-go/pkg/v2" - "github.com/selectel/go-selvpcclient/v4/selvpcclient" - "golang.org/x/net/idna" + "github.com/selectel/go-selvpcclient/v3/selvpcclient" +) + +const tokenHeader = "X-Auth-Token" + +const ( + defaultBaseURL = "https://api.selectel.ru/domains/v2" + defaultTTL = 60 + defaultPropagationTimeout = 120 * time.Second + defaultPollingInterval = 5 * time.Second + defaultHTTPTimeout = 30 * time.Second ) const ( envNamespace = "SELECTELV2_" - EnvBaseURL = envNamespace + "BASE_URL" - EnvUsernameOS = envNamespace + "USERNAME" - EnvPasswordOS = envNamespace + "PASSWORD" - EnvDomainName = envNamespace + "ACCOUNT_ID" - EnvProjectID = envNamespace + "PROJECT_ID" - EnvAuthRegion = envNamespace + "AUTH_REGION" - EnvAuthURL = envNamespace + "AUTH_URL" - EnvUserDomainName = envNamespace + "USER_DOMAIN_NAME" + EnvBaseURL = envNamespace + "BASE_URL" + EnvUsernameOS = envNamespace + "USERNAME" + EnvPasswordOS = envNamespace + "PASSWORD" + EnvAccount = envNamespace + "ACCOUNT_ID" + EnvProjectID = envNamespace + "PROJECT_ID" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" @@ -37,34 +41,15 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const ( - defaultBaseURL = "https://api.selectel.ru/domains/v2" - defaultAuthRegion = "ru-1" - defaultAuthURL = "https://cloud.api.selcloud.ru/identity/v3/" -) - -const ( - defaultTTL = 60 - defaultPropagationTimeout = 120 * time.Second - defaultPollingInterval = 5 * time.Second - defaultHTTPTimeout = 30 * time.Second -) - -const tokenHeader = "X-Auth-Token" - var errNotFound = errors.New("rrset not found") // Config is used to configure the creation of the DNSProvider. type Config struct { - BaseURL string - Username string - Password string - DomainName string - ProjectID string - AuthURL string - AuthRegion string - UserDomainName string - + BaseURL string + Username string + Password string + Account string + ProjectID string TTL int PropagationTimeout time.Duration PollingInterval time.Duration @@ -74,10 +59,7 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - BaseURL: env.GetOrDefaultString(EnvBaseURL, defaultBaseURL), - AuthRegion: env.GetOrDefaultString(EnvAuthRegion, defaultAuthRegion), - AuthURL: env.GetOrDefaultString(EnvAuthURL, defaultAuthURL), - + BaseURL: env.GetOrDefaultString(EnvBaseURL, selectel.DefaultSelectelBaseURL), TTL: env.GetOrDefaultInt(EnvTTL, defaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, defaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, defaultPollingInterval), @@ -94,7 +76,7 @@ type DNSProvider struct { // NewDNSProvider returns a DNSProvider instance configured for Selectel Domains APIv2. func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvUsernameOS, EnvPasswordOS, EnvDomainName, EnvProjectID) + values, err := env.Get(EnvUsernameOS, EnvPasswordOS, EnvAccount, EnvProjectID) if err != nil { return nil, fmt.Errorf("selectelv2: %w", err) } @@ -102,9 +84,8 @@ func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() config.Username = values[EnvUsernameOS] config.Password = values[EnvPasswordOS] - config.DomainName = values[EnvDomainName] + config.Account = values[EnvAccount] config.ProjectID = values[EnvProjectID] - config.UserDomainName = env.GetOrDefaultString(EnvUserDomainName, "") return NewDNSProviderConfig(config) } @@ -123,8 +104,8 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("selectelv2: missing password") } - if config.DomainName == "" { - return nil, errors.New("selectelv2: missing account ID") + if config.Account == "" { + return nil, errors.New("selectelv2: missing account") } if config.ProjectID == "" { @@ -132,25 +113,25 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { } headers := http.Header{} - useragent.SetHeader(headers) + headers.Set("User-Agent", "lego/selectelv2") return &DNSProvider{ - baseClient: selectelapi.NewClient(config.BaseURL, clientdebug.Wrap(config.HTTPClient), headers), + baseClient: selectelapi.NewClient(defaultBaseURL, config.HTTPClient, headers), config: config, }, nil } // Timeout returns the Timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval +func (p *DNSProvider) Timeout() (timeout, interval time.Duration) { + return p.config.PropagationTimeout, p.config.PollingInterval } // Present creates a TXT record to fulfill DNS-01 challenge. -func (d *DNSProvider) Present(domain, _, keyAuth string) error { +func (p *DNSProvider) Present(domain, _, keyAuth string) error { ctx := context.Background() - client, err := d.authorize(ctx) + client, err := p.authorize() if err != nil { return fmt.Errorf("selectelv2: authorize: %w", err) } @@ -171,7 +152,7 @@ func (d *DNSProvider) Present(domain, _, keyAuth string) error { newRRSet := &selectelapi.RRSet{ Name: info.EffectiveFQDN, Type: selectelapi.TXT, - TTL: d.config.TTL, + TTL: p.config.TTL, Records: []selectelapi.RecordItem{{Content: fmt.Sprintf("%q", info.Value)}}, } @@ -194,10 +175,10 @@ func (d *DNSProvider) Present(domain, _, keyAuth string) error { } // CleanUp removes a TXT record used for DNS-01 challenge. -func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { +func (p *DNSProvider) CleanUp(domain, _, keyAuth string) error { ctx := context.Background() - client, err := d.authorize(ctx) + client, err := p.authorize() if err != nil { return fmt.Errorf("selectelv2: authorize: %w", err) } @@ -238,8 +219,8 @@ func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { return nil } -func (d *DNSProvider) authorize(ctx context.Context) (*clientWrapper, error) { - token, err := obtainOpenstackToken(ctx, d.config) +func (p *DNSProvider) authorize() (*clientWrapper, error) { + token, err := obtainOpenstackToken(p.config) if err != nil { return nil, err } @@ -248,20 +229,16 @@ func (d *DNSProvider) authorize(ctx context.Context) (*clientWrapper, error) { extraHeaders.Set(tokenHeader, token) return &clientWrapper{ - DNSClient: d.baseClient.WithHeaders(extraHeaders), + DNSClient: p.baseClient.WithHeaders(extraHeaders), }, nil } -func obtainOpenstackToken(ctx context.Context, config *Config) (string, error) { +func obtainOpenstackToken(config *Config) (string, error) { vpcClient, err := selvpcclient.NewClient(&selvpcclient.ClientOptions{ - Context: ctx, - DomainName: config.DomainName, - AuthURL: config.AuthURL, - AuthRegion: config.AuthRegion, Username: config.Username, Password: config.Password, + UserDomainName: config.Account, ProjectID: config.ProjectID, - UserDomainName: config.UserDomainName, }) if err != nil { return "", fmt.Errorf("new VPC client: %w", err) @@ -275,12 +252,7 @@ type clientWrapper struct { } func (w *clientWrapper) getZone(ctx context.Context, name string) (*selectelapi.Zone, error) { - unicodeName, err := idna.ToUnicode(name) - if err != nil { - return nil, fmt.Errorf("to unicode: %w", err) - } - - params := &map[string]string{"filter": unicodeName} + params := &map[string]string{"filter": name} zones, err := w.ListZones(ctx, params) if err != nil { @@ -288,28 +260,23 @@ func (w *clientWrapper) getZone(ctx context.Context, name string) (*selectelapi. } for _, zone := range zones.GetItems() { - if zone.Name == dns.Fqdn(unicodeName) { + if zone.Name == dns01.ToFqdn(name) { return zone, nil } } if len(strings.Split(dns01.UnFqdn(name), ".")) == 1 { - return nil, fmt.Errorf("zone '%s' for challenge has not been found", name) + return nil, errors.New("zone for challenge has not been found") } - // after is always defined since if no dots present we exit above. - _, after, _ := strings.Cut(name, ".") + // -1 can not be returned since if no dots present we exit above + i := strings.Index(name, ".") - return w.getZone(ctx, after) + return w.getZone(ctx, name[i+1:]) } func (w *clientWrapper) getRRset(ctx context.Context, name, zoneID string) (*selectelapi.RRSet, error) { - unicodeName, err := idna.ToUnicode(name) - if err != nil { - return nil, fmt.Errorf("to unicode: %w", err) - } - - params := &map[string]string{"name": unicodeName, "rrset_types": string(selectelapi.TXT)} + params := &map[string]string{"name": name, "rrset_types": string(selectelapi.TXT)} resp, err := w.ListRRSets(ctx, zoneID, params) if err != nil { @@ -317,7 +284,7 @@ func (w *clientWrapper) getRRset(ctx context.Context, name, zoneID string) (*sel } for _, rrset := range resp.GetItems() { - if rrset.Name == dns.Fqdn(unicodeName) { + if rrset.Name == dns01.ToFqdn(name) { return rrset, nil } } diff --git a/providers/dns/selectelv2/selectelv2.toml b/providers/dns/selectelv2/selectelv2.toml index 480c7756e..7870688bd 100644 --- a/providers/dns/selectelv2/selectelv2.toml +++ b/providers/dns/selectelv2/selectelv2.toml @@ -5,11 +5,11 @@ Code = "selectelv2" Since = "v4.17.0" Example = ''' -SELECTELV2_USERNAME=trex \ -SELECTELV2_PASSWORD=xxxxx \ -SELECTELV2_ACCOUNT_ID=1234567 \ -SELECTELV2_PROJECT_ID=111a11111aaa11aa1a11aaa11111aa1a \ -lego --dns selectelv2 -d '*.example.com' -d example.com run +SELECTEL_USERNAME=trex \ +SELECTEL_PASSWORD=xxxxx \ +SELECTEL_ACCOUNT_ID=1234567 \ +SELECTEL_PROJECT_ID=111a11111aaa11aa1a11aaa11111aa1a \ +lego --email you@example.com --dns selectelv2 --domains my.example.org run ''' [Configuration] @@ -20,13 +20,10 @@ lego --dns selectelv2 -d '*.example.com' -d example.com run SELECTELV2_PROJECT_ID = "Cloud project ID (UUID)" [Configuration.Additional] SELECTELV2_BASE_URL = "API endpoint URL" - SELECTELV2_AUTH_REGION = "Location for auth endpoint like ResellAPI or Keystone (default: 'ru-1')" - SELECTELV2_AUTH_URL = "Identity endpoint (defaul: 'https://cloud.api.selcloud.ru/identity/v3/')" - SELECTELV2_USER_DOMAIN_NAME = "To specify the domain name (account ID) where the user is located. (default: SELECTELV2_ACCOUNT_ID)" - SELECTELV2_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 5)" - SELECTELV2_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - SELECTELV2_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" - SELECTELV2_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + SELECTELV2_POLLING_INTERVAL = "Time between DNS propagation check" + SELECTELV2_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + SELECTELV2_TTL = "The TTL of the TXT record used for the DNS challenge" + SELECTELV2_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://developers.selectel.ru/docs/cloud-services/dns_api/dns_api_actual/" diff --git a/providers/dns/selectelv2/selectelv2_test.go b/providers/dns/selectelv2/selectelv2_test.go index 2627fa023..4859b9932 100644 --- a/providers/dns/selectelv2/selectelv2_test.go +++ b/providers/dns/selectelv2/selectelv2_test.go @@ -11,15 +11,7 @@ import ( const envDomain = envNamespace + "DOMAIN" -var envTest = tester.NewEnvTest( - EnvUsernameOS, - EnvPasswordOS, - EnvDomainName, - EnvUserDomainName, - EnvProjectID, - EnvAuthRegion, - EnvAuthURL, -). +var envTest = tester.NewEnvTest(EnvUsernameOS, EnvPasswordOS, EnvAccount, EnvProjectID). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { @@ -33,7 +25,7 @@ func TestNewDNSProvider(t *testing.T) { envVars: map[string]string{ EnvUsernameOS: "someName", EnvPasswordOS: "qwerty", - EnvDomainName: "1", + EnvAccount: "1", EnvProjectID: "111a11111aaa11aa1a11aaa11111aa1a", }, }, @@ -41,7 +33,7 @@ func TestNewDNSProvider(t *testing.T) { desc: "missing username", envVars: map[string]string{ EnvPasswordOS: "qwerty", - EnvDomainName: "1", + EnvAccount: "1", EnvProjectID: "111a11111aaa11aa1a11aaa11111aa1a", }, expected: "selectelv2: some credentials information are missing: SELECTELV2_USERNAME", @@ -50,7 +42,7 @@ func TestNewDNSProvider(t *testing.T) { desc: "missing password", envVars: map[string]string{ EnvUsernameOS: "someName", - EnvDomainName: "1", + EnvAccount: "1", EnvProjectID: "111a11111aaa11aa1a11aaa11111aa1a", }, expected: "selectelv2: some credentials information are missing: SELECTELV2_PASSWORD", @@ -69,7 +61,7 @@ func TestNewDNSProvider(t *testing.T) { envVars: map[string]string{ EnvUsernameOS: "someName", EnvPasswordOS: "qwerty", - EnvDomainName: "1", + EnvAccount: "1", }, expected: "selectelv2: some credentials information are missing: SELECTELV2_PROJECT_ID", }, @@ -78,7 +70,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -132,7 +123,7 @@ func TestNewDNSProviderConfig(t *testing.T) { username: "user", password: "secret", projectID: "111a11111aaa11aa1a11aaa11111aa1a", - expected: "selectelv2: missing account ID", + expected: "selectelv2: missing account", }, { desc: "missing projectID", @@ -148,7 +139,7 @@ func TestNewDNSProviderConfig(t *testing.T) { config := NewDefaultConfig() config.Username = test.username config.Password = test.password - config.DomainName = test.account + config.Account = test.account config.ProjectID = test.projectID p, err := NewDNSProviderConfig(config) @@ -171,7 +162,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -185,7 +175,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/selfhostde/internal/client.go b/providers/dns/selfhostde/internal/client.go deleted file mode 100644 index 7eeca20a9..000000000 --- a/providers/dns/selfhostde/internal/client.go +++ /dev/null @@ -1,66 +0,0 @@ -package internal - -import ( - "context" - "fmt" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" -) - -const defaultBaseURL = "https://selfhost.de/cgi-bin/api.pl" - -// Client the SelfHost client. -type Client struct { - username string - password string - - baseURL string - HTTPClient *http.Client -} - -// NewClient Creates a new Client. -func NewClient(username, password string) *Client { - return &Client{ - username: username, - password: password, - baseURL: defaultBaseURL, - HTTPClient: &http.Client{Timeout: 5 * time.Second}, - } -} - -// UpdateTXTRecord updates content of an existing TXT record. -func (c *Client) UpdateTXTRecord(ctx context.Context, recordID, content string) error { - endpoint, err := url.Parse(c.baseURL) - if err != nil { - return fmt.Errorf("parse URL: %w", err) - } - - query := endpoint.Query() - query.Set("username", c.username) - query.Set("password", c.password) - query.Set("rid", recordID) - query.Set("content", content) - - endpoint.RawQuery = query.Encode() - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) - if err != nil { - return fmt.Errorf("new HTTP request: %w", err) - } - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return errutils.NewUnexpectedResponseStatusCodeError(req, resp) - } - - return nil -} diff --git a/providers/dns/selfhostde/internal/client_test.go b/providers/dns/selfhostde/internal/client_test.go deleted file mode 100644 index 22949728c..000000000 --- a/providers/dns/selfhostde/internal/client_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -func setupClient(server *httptest.Server) (*Client, error) { - client := NewClient("user", "secret") - client.baseURL = server.URL - client.HTTPClient = server.Client() - - return client, nil -} - -func TestClient_UpdateTXTRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /", nil, servermock.CheckQueryParameter().Strict(). - With("rid", "123456"). - With("content", "txt"). - With("username", "user"). - With("password", "secret"), - ). - Build(t) - - err := client.UpdateTXTRecord(t.Context(), "123456", "txt") - require.NoError(t, err) -} - -func TestClient_UpdateTXTRecord_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /", servermock.Noop().WithStatusCode(http.StatusBadRequest)). - Build(t) - - err := client.UpdateTXTRecord(t.Context(), "123456", "txt") - require.EqualError(t, err, "unexpected status code: [status code: 400] body: ") -} diff --git a/providers/dns/selfhostde/internal/readme.md b/providers/dns/selfhostde/internal/readme.md deleted file mode 100644 index d0b01bfe4..000000000 --- a/providers/dns/selfhostde/internal/readme.md +++ /dev/null @@ -1,7 +0,0 @@ -# SelfHost.(de|eu) - -SelfHost doesn't provide an official API documentation and there are no endpoints for create a TXT record or delete a TXT record. - -## More - -The documentation found at https://kirk.selfhost.de/cgi-bin/selfhost?p=document&name=api (PDF) describes the DynDNS/ddns API endpoint and is not used by our client. diff --git a/providers/dns/selfhostde/mapping.go b/providers/dns/selfhostde/mapping.go deleted file mode 100644 index fe11ceda1..000000000 --- a/providers/dns/selfhostde/mapping.go +++ /dev/null @@ -1,133 +0,0 @@ -package selfhostde - -import ( - "errors" - "fmt" - "strings" -) - -const ( - lineSep = "," - recordSep = ":" -) - -type Seq struct { - cursor int - ids []string -} - -func NewSeq(ids ...string) *Seq { - return &Seq{ids: ids} -} - -func (s *Seq) Next() string { - if len(s.ids) == 1 { - return s.ids[0] - } - - v := s.ids[s.cursor] - - if s.cursor < len(s.ids)-1 { - s.cursor++ - } else { - s.cursor = 0 - } - - return v -} - -func parseRecordsMapping(raw string) (map[string]*Seq, error) { - raw = strings.ReplaceAll(raw, " ", "") - - if raw == "" { - return nil, errors.New("empty mapping") - } - - acc := map[string]*Seq{} - - for { - index, err := safeIndex(raw, lineSep) - if err != nil { - return nil, err - } - - if index != -1 { - name, seq, err := parseLine(raw[:index]) - if err != nil { - return nil, err - } - - acc[name] = seq - - // Data for the next iteration. - raw = raw[index+1:] - - continue - } - - name, seq, errP := parseLine(raw) - if errP != nil { - return nil, errP - } - - acc[name] = seq - - return acc, nil - } -} - -func parseLine(line string) (string, *Seq, error) { - idx, err := safeIndex(line, recordSep) - if err != nil { - return "", nil, err - } - - if idx == -1 { - return "", nil, fmt.Errorf("missing %q: %s", recordSep, line) - } - - name, rawIDs := line[:idx], line[idx+1:] - - var ( - ids []string - count int - ) - - for { - idx, err = safeIndex(rawIDs, recordSep) - if err != nil { - return "", nil, err - } - - if count == 2 { - return "", nil, fmt.Errorf("too many record IDs for one domain: %s", line) - } - - if idx != -1 { - ids = append(ids, rawIDs[:idx]) - count++ - - // Data for the next iteration. - rawIDs = rawIDs[idx+1:] - - continue - } - - ids = append(ids, rawIDs) - - return name, NewSeq(ids...), nil - } -} - -func safeIndex(v, sep string) (int, error) { - index := strings.Index(v, sep) - if index == 0 { - return 0, fmt.Errorf("first char is %q: %s", sep, v) - } - - if index == len(v)-1 { - return 0, fmt.Errorf("last char is %q: %s", sep, v) - } - - return index, nil -} diff --git a/providers/dns/selfhostde/mapping_test.go b/providers/dns/selfhostde/mapping_test.go deleted file mode 100644 index 22bf684d7..000000000 --- a/providers/dns/selfhostde/mapping_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package selfhostde - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func Test_parseRecordsMapping(t *testing.T) { - testCases := []struct { - desc string - rawData string - expected map[string]*Seq - }{ - { - desc: "one domain, one record id", - rawData: "example.com:123", - expected: map[string]*Seq{ - "example.com": NewSeq("123"), - }, - }, - { - desc: "several domain, one record id", - rawData: "example.com:123, example.org:456,foo.example.com:789", - expected: map[string]*Seq{ - "example.com": NewSeq("123"), - "example.org": NewSeq("456"), - "foo.example.com": NewSeq("789"), - }, - }, - { - desc: "one domain, 2 record ids", - rawData: "example.com:123:456", - expected: map[string]*Seq{ - "example.com": NewSeq("123", "456"), - }, - }, - { - desc: "several domain, 2 record ids", - rawData: "example.com:123:321, example.org:456:654,foo.example.com:789:987", - expected: map[string]*Seq{ - "example.com": NewSeq("123", "321"), - "example.org": NewSeq("456", "654"), - "foo.example.com": NewSeq("789", "987"), - }, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - mapping, err := parseRecordsMapping(test.rawData) - require.NoError(t, err) - - assert.Equal(t, test.expected, mapping) - }) - } -} - -func Test_parseRecordsMapping_error(t *testing.T) { - testCases := []struct { - desc string - rawData string - expected string - }{ - { - desc: "empty", - rawData: "", - expected: "empty mapping", - }, - { - desc: "only spaces", - rawData: " ", - expected: "empty mapping", - }, - { - desc: "one domain, no record id", - rawData: "example.com", - expected: `missing ":": example.com`, - }, - { - desc: "one domain, more than 2 record ids", - rawData: "example.com:123:456:789", - expected: "too many record IDs for one domain: example.com:123:456:789", - }, - { - desc: "several domain, more than 2 record ids", - rawData: "example.com:123, example.org:456:789:147", - expected: "too many record IDs for one domain: example.org:456:789:147", - }, - { - desc: "no ids, ends with 2 dots", - rawData: "example.com:", - expected: `last char is ":": example.com:`, - }, - { - desc: "no ids,starts with 2 dots", - rawData: ":example.com", - expected: `first char is ":": :example.com`, - }, - { - desc: "with ids but ends with 2 dots", - rawData: "example.com:123:", - expected: `last char is ":": 123:`, - }, - { - desc: "only 2 dots", - rawData: ":", - expected: `first char is ":": :`, - }, - { - desc: "only comma", - rawData: ",", - expected: `first char is ",": ,`, - }, - { - desc: "ends with comma", - rawData: "example.com,", - expected: `last char is ",": example.com,`, - }, - { - desc: "combo", - rawData: "::::,::", - expected: `first char is ":": ::::`, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - _, err := parseRecordsMapping(test.rawData) - require.EqualError(t, err, test.expected) - }) - } -} - -func TestSeq_Next(t *testing.T) { - testCases := []struct { - desc string - ids []string - expected []string - }{ - { - desc: "one value", - ids: []string{"a"}, - expected: []string{"a", "a", "a"}, - }, - { - desc: "two values", - ids: []string{"a", "b"}, - expected: []string{"a", "b", "a", "b"}, - }, - { - desc: "three values", - ids: []string{"a", "b", "c"}, - expected: []string{"a", "b", "c", "a", "b", "c", "a"}, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - seq := NewSeq(test.ids...) - for _, s := range test.expected { - assert.Equal(t, s, seq.Next()) - } - }) - } -} diff --git a/providers/dns/selfhostde/selfhostde.go b/providers/dns/selfhostde/selfhostde.go deleted file mode 100644 index 035cd5363..000000000 --- a/providers/dns/selfhostde/selfhostde.go +++ /dev/null @@ -1,194 +0,0 @@ -// Package selfhostde implements a DNS provider for solving the DNS-01 challenge using SelfHost.(de|eu). -package selfhostde - -import ( - "context" - "errors" - "fmt" - "net/http" - "strings" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/selfhostde/internal" -) - -// Environment variables. -const ( - envNamespace = "SELFHOSTDE_" - - EnvUsername = envNamespace + "USERNAME" - EnvPassword = envNamespace + "PASSWORD" - EnvRecordsMapping = envNamespace + "RECORDS_MAPPING" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - Username string - Password string - - RecordsMapping map[string]*Seq - recordsMappingMu sync.Mutex - - TTL int - PropagationTimeout time.Duration - PollingInterval time.Duration - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 4*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 30*time.Second), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -func (c *Config) getSeqNext(domain string) (string, error) { - effectiveDomain := strings.TrimPrefix(domain, "_acme-challenge.") - - c.recordsMappingMu.Lock() - defer c.recordsMappingMu.Unlock() - - seq, ok := c.RecordsMapping[effectiveDomain] - if !ok { - // fallback - seq, ok = c.RecordsMapping[domain] - if !ok { - return "", fmt.Errorf("record mapping not found for %q", effectiveDomain) - } - } - - return seq.Next(), nil -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client - - recordIDs map[string]string - recordIDsMu sync.Mutex -} - -// NewDNSProvider returns a DNSProvider instance configured for SelfHost.(de|eu). -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvUsername, EnvPassword, EnvRecordsMapping) - if err != nil { - return nil, fmt.Errorf("selfhostde: %w", err) - } - - config := NewDefaultConfig() - config.Username = values[EnvUsername] - config.Password = values[EnvPassword] - - mapping, err := parseRecordsMapping(values[EnvRecordsMapping]) - if err != nil { - return nil, fmt.Errorf("selfhostde: malformed records mapping: %w", err) - } - - config.RecordsMapping = mapping - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for SelfHost.(de|eu). -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("selfhostde: supplied configuration is nil") - } - - if config.Username == "" || config.Password == "" { - return nil, errors.New("selfhostde: credentials missing") - } - - if len(config.RecordsMapping) == 0 { - return nil, errors.New("selfhostde: missing record mapping") - } - - for domain, seq := range config.RecordsMapping { - if seq == nil || len(seq.ids) == 0 { - return nil, fmt.Errorf("selfhostde: missing record ID for %q", domain) - } - } - - client := internal.NewClient(config.Username, config.Password) - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - recordIDs: make(map[string]string), - }, nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -// Present creates a TXT record to fulfill the dns-01 challenge. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - recordID, err := d.config.getSeqNext(dns01.UnFqdn(info.EffectiveFQDN)) - if err != nil { - return fmt.Errorf("selfhostde: %w", err) - } - - err = d.client.UpdateTXTRecord(context.Background(), recordID, info.Value) - if err != nil { - return fmt.Errorf("selfhostde: update DNS TXT record (id=%s): %w", recordID, err) - } - - d.recordIDsMu.Lock() - d.recordIDs[token] = recordID - d.recordIDsMu.Unlock() - - return nil -} - -// CleanUp removes the TXT record previously created. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - d.recordIDsMu.Lock() - recordID, ok := d.recordIDs[token] - d.recordIDsMu.Unlock() - - if !ok { - return fmt.Errorf("selfhostde: unknown record ID for %q", dns01.UnFqdn(info.EffectiveFQDN)) - } - - err := d.client.UpdateTXTRecord(context.Background(), recordID, "empty") - if err != nil { - return fmt.Errorf("selfhostde: emptied DNS TXT record (id=%s): %w", recordID, err) - } - - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - - return nil -} diff --git a/providers/dns/selfhostde/selfhostde.toml b/providers/dns/selfhostde/selfhostde.toml deleted file mode 100644 index bd22c6c41..000000000 --- a/providers/dns/selfhostde/selfhostde.toml +++ /dev/null @@ -1,54 +0,0 @@ -Name = "SelfHost.(de|eu)" -Description = '''''' -URL = "https://www.selfhost.de" -Code = "selfhostde" -Since = "v4.19.0" - -Example = ''' -SELFHOSTDE_USERNAME=xxx \ -SELFHOSTDE_PASSWORD=yyy \ -SELFHOSTDE_RECORDS_MAPPING=my.example.com:123 \ -lego --dns selfhostde -d '*.example.com' -d example.com run -''' - -Additional = """ -SelfHost.de doesn't have an API to create or delete TXT records, -there is only an "unofficial" and undocumented endpoint to update an existing TXT record. - -So, before using lego to request a certificate for a given domain or wildcard (such as `my.example.org` or `*.my.example.org`), -you must create: - -- one TXT record named `_acme-challenge.my.example.org` if you are **not** using wildcard for this domain. -- two TXT records named `_acme-challenge.my.example.org` if you are using wildcard for this domain. - -After that you must edit the TXT record(s) to get the ID(s). - -You then must prepare the `SELFHOSTDE_RECORDS_MAPPING` environment variable with the following format: - -``` -::,::,:: -``` - -where each group of domain + record ID(s) is separated with a comma (`,`), -and the domain and record ID(s) are separated with a colon (`:`). - -For example, if you want to create or renew a certificate for `my.example.org`, `*.my.example.org`, and `other.example.org`, -you would need: - -- two separate records for `_acme-challenge.my.example.org` -- and another separate record for `_acme-challenge.other.example.org` - -The resulting environment variable would then be: `SELFHOSTDE_RECORDS_MAPPING=my.example.com:123:456,other.example.com:789` - -""" - -[Configuration] - [Configuration.Credentials] - SELFHOSTDE_USERNAME = "Username" - SELFHOSTDE_PASSWORD = "Password" - SELFHOSTDE_RECORDS_MAPPING = "Record IDs mapping with domains (ex: example.com:123:456,example.org:789,foo.example.com:147)" - [Configuration.Additional] - SELFHOSTDE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 30)" - SELFHOSTDE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 240)" - SELFHOSTDE_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - SELFHOSTDE_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" diff --git a/providers/dns/selfhostde/selfhostde_test.go b/providers/dns/selfhostde/selfhostde_test.go deleted file mode 100644 index 7c12195fa..000000000 --- a/providers/dns/selfhostde/selfhostde_test.go +++ /dev/null @@ -1,211 +0,0 @@ -package selfhostde - -import ( - "testing" - "time" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvUsername, EnvPassword, EnvRecordsMapping). - WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvUsername: "user", - EnvPassword: "secret", - EnvRecordsMapping: "example.com:123", - }, - }, - { - desc: "missing username", - envVars: map[string]string{ - EnvPassword: "secret", - EnvRecordsMapping: "example.com:123", - }, - expected: "selfhostde: some credentials information are missing: SELFHOSTDE_USERNAME", - }, - { - desc: "missing password", - envVars: map[string]string{ - EnvUsername: "user", - EnvRecordsMapping: "example.com:123", - }, - expected: "selfhostde: some credentials information are missing: SELFHOSTDE_PASSWORD", - }, - { - desc: "missing records mapping", - envVars: map[string]string{ - EnvUsername: "user", - EnvPassword: "secret", - }, - expected: "selfhostde: some credentials information are missing: SELFHOSTDE_RECORDS_MAPPING", - }, - { - desc: "invalid records mapping", - envVars: map[string]string{ - EnvUsername: "user", - EnvPassword: "secret", - EnvRecordsMapping: "example.com", - }, - expected: `selfhostde: malformed records mapping: missing ":": example.com`, - }, - { - desc: "missing information", - envVars: map[string]string{}, - expected: "selfhostde: some credentials information are missing: SELFHOSTDE_USERNAME,SELFHOSTDE_PASSWORD,SELFHOSTDE_RECORDS_MAPPING", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - assert.NotNil(t, p.config) - assert.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - username string - password string - recordMapping map[string]*Seq - expected string - }{ - { - desc: "success", - username: "user", - password: "secret", - recordMapping: map[string]*Seq{ - "example.com": NewSeq("123"), - }, - }, - { - desc: "missing username", - password: "secret", - recordMapping: map[string]*Seq{ - "example.com": NewSeq("123"), - }, - expected: "selfhostde: credentials missing", - }, - { - desc: "missing password", - username: "user", - recordMapping: map[string]*Seq{ - "example.com": NewSeq("123"), - }, - expected: "selfhostde: credentials missing", - }, - { - desc: "missing sequence", - username: "user", - password: "secret", - recordMapping: map[string]*Seq{ - "example.com": nil, - }, - expected: `selfhostde: missing record ID for "example.com"`, - }, - { - desc: "empty sequence", - username: "user", - password: "secret", - recordMapping: map[string]*Seq{ - "example.com": NewSeq(), - }, - expected: `selfhostde: missing record ID for "example.com"`, - }, - { - desc: "missing records mapping", - username: "user", - password: "secret", - expected: "selfhostde: missing record mapping", - }, - { - desc: "empty records mapping", - username: "user", - password: "secret", - recordMapping: map[string]*Seq{}, - expected: "selfhostde: missing record mapping", - }, - { - desc: "missing information", - expected: "selfhostde: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.Username = test.username - config.Password = test.password - config.RecordsMapping = test.recordMapping - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - assert.NotNil(t, p.config) - assert.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - time.Sleep(2 * time.Second) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/servercow/internal/client.go b/providers/dns/servercow/internal/client.go index e15237201..3695b0979 100644 --- a/providers/dns/servercow/internal/client.go +++ b/providers/dns/servercow/internal/client.go @@ -47,7 +47,6 @@ func (c *Client) GetRecords(ctx context.Context, domain string) ([]Record, error } var records []Record - err = c.do(req, &records) if err != nil { return nil, err @@ -66,7 +65,6 @@ func (c *Client) CreateUpdateRecord(ctx context.Context, domain string, data Rec } var msg Message - err = c.do(req, &msg) if err != nil { return nil, err @@ -89,7 +87,6 @@ func (c *Client) DeleteRecord(ctx context.Context, domain string, data Record) ( } var msg Message - err = c.do(req, &msg) if err != nil { return nil, err @@ -171,7 +168,6 @@ func unmarshal(raw []byte, v any) error { } var apiErr Message - errU := json.Unmarshal(raw, &apiErr) if errU != nil { return fmt.Errorf("unmarshaling %T error: %w: %s", v, err, string(raw)) diff --git a/providers/dns/servercow/internal/client_test.go b/providers/dns/servercow/internal/client_test.go index 3733ccad1..484649281 100644 --- a/providers/dns/servercow/internal/client_test.go +++ b/providers/dns/servercow/internal/client_test.go @@ -1,38 +1,57 @@ package internal import ( + "context" "encoding/json" + "io" + "net/http" "net/http/httptest" "net/url" "os" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("user", "secret") - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - With("X-Auth-Username", "user"). - With("X-Auth-Password", "secret"), - ) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient("", "") + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client, mux } func TestClient_GetRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /example.com", servermock.ResponseFromFixture("records-01.json")). - Build(t) + client, handler := setupTest(t) - records, err := client.GetRecords(t.Context(), "example.com") + handler.HandleFunc("/lego.wtf", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + file, err := os.Open("./fixtures/records-01.json") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + records, err := client.GetRecords(context.Background(), "lego.wtf") require.NoError(t, err) recordsJSON, err := json.Marshal(records) @@ -45,22 +64,55 @@ func TestClient_GetRecords(t *testing.T) { } func TestClient_GetRecords_error(t *testing.T) { - client := mockBuilder(). - Route("GET /example.com", servermock.JSONEncode(Message{ErrorMsg: "authentication failed"})). - Build(t) + client, handler := setupTest(t) - records, err := client.GetRecords(t.Context(), "example.com") + handler.HandleFunc("/lego.wtf", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + err := json.NewEncoder(rw).Encode(Message{ErrorMsg: "authentication failed"}) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + records, err := client.GetRecords(context.Background(), "lego.wtf") require.Error(t, err) assert.Nil(t, records) } func TestClient_CreateUpdateRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /example.com", - servermock.JSONEncode(Message{Message: "ok"}), - servermock.CheckRequestJSONBody(`{"name":"_acme-challenge.www","type":"TXT","ttl":30,"content":["aaa","bbb"]}`)). - Build(t) + client, handler := setupTest(t) + + handler.HandleFunc("/lego.wtf", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + content, err := io.ReadAll(req.Body) + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + expectedRequest := `{"name":"_acme-challenge.www","type":"TXT","ttl":30,"content":["aaa","bbb"]}` + + if !assert.JSONEq(t, expectedRequest, string(content)) { + http.Error(rw, "invalid content", http.StatusBadRequest) + return + } + + err = json.NewEncoder(rw).Encode(Message{Message: "ok"}) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) record := Record{ Name: "_acme-challenge.www", @@ -69,7 +121,7 @@ func TestClient_CreateUpdateRecord(t *testing.T) { Content: Value{"aaa", "bbb"}, } - msg, err := client.CreateUpdateRecord(t.Context(), "example.com", record) + msg, err := client.CreateUpdateRecord(context.Background(), "lego.wtf", record) require.NoError(t, err) expected := &Message{Message: "ok"} @@ -77,34 +129,66 @@ func TestClient_CreateUpdateRecord(t *testing.T) { } func TestClient_CreateUpdateRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /example.com", - servermock.JSONEncode(Message{ErrorMsg: "parameter type must be cname, txt, tlsa, caa, a or aaaa"})). - Build(t) + client, handler := setupTest(t) + + handler.HandleFunc("/lego.wtf", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + err := json.NewEncoder(rw).Encode(Message{ErrorMsg: "parameter type must be cname, txt, tlsa, caa, a or aaaa"}) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) record := Record{ Name: "_acme-challenge.www", } - msg, err := client.CreateUpdateRecord(t.Context(), "example.com", record) + msg, err := client.CreateUpdateRecord(context.Background(), "lego.wtf", record) require.Error(t, err) - assert.Nil(t, msg) + assert.Nil(t, msg) //nolint:testifylint // false positive https://github.com/Antonboom/testifylint/issues/95 } func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /example.com", - servermock.JSONEncode(Message{Message: "ok"}), - servermock.CheckRequestJSONBody(`{"name":"_acme-challenge.www","type":"TXT"}`)). - Build(t) + client, handler := setupTest(t) + + handler.HandleFunc("/lego.wtf", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodDelete { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + content, err := io.ReadAll(req.Body) + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + expectedRequest := `{"name":"_acme-challenge.www","type":"TXT"}` + + if !assert.JSONEq(t, expectedRequest, string(content)) { + http.Error(rw, "invalid content", http.StatusBadRequest) + return + } + + err = json.NewEncoder(rw).Encode(Message{Message: "ok"}) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) record := Record{ Name: "_acme-challenge.www", Type: "TXT", } - msg, err := client.DeleteRecord(t.Context(), "example.com", record) + msg, err := client.DeleteRecord(context.Background(), "lego.wtf", record) require.NoError(t, err) expected := &Message{Message: "ok"} @@ -112,17 +196,27 @@ func TestClient_DeleteRecord(t *testing.T) { } func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /example.com", - servermock.JSONEncode(Message{ErrorMsg: "parameter type must be cname, txt, tlsa, caa, a or aaaa"})). - Build(t) + client, handler := setupTest(t) + + handler.HandleFunc("/lego.wtf", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodDelete { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + err := json.NewEncoder(rw).Encode(Message{ErrorMsg: "parameter type must be cname, txt, tlsa, caa, a or aaaa"}) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) record := Record{ Name: "_acme-challenge.www", } - msg, err := client.DeleteRecord(t.Context(), "example.com", record) + msg, err := client.DeleteRecord(context.Background(), "lego.wtf", record) require.Error(t, err) - assert.Nil(t, msg) + assert.Nil(t, msg) //nolint:testifylint // false positive https://github.com/Antonboom/testifylint/issues/95 } diff --git a/providers/dns/servercow/internal/types.go b/providers/dns/servercow/internal/types.go index 9a951e806..5a8fb6ff8 100644 --- a/providers/dns/servercow/internal/types.go +++ b/providers/dns/servercow/internal/types.go @@ -43,7 +43,6 @@ func (v *Value) UnmarshalJSON(b []byte) error { } *v = append(*v, s) - return nil } diff --git a/providers/dns/servercow/servercow.go b/providers/dns/servercow/servercow.go index 557c6b1ec..3a8ae1f2a 100644 --- a/providers/dns/servercow/servercow.go +++ b/providers/dns/servercow/servercow.go @@ -9,10 +9,8 @@ import ( "slices" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/servercow/internal" ) @@ -29,8 +27,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Username string @@ -45,7 +41,7 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + TTL: env.GetOrDefaultInt(EnvTTL, 120), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ @@ -86,8 +82,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, client: client, @@ -140,7 +134,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("servercow: failed to update TXT records: %w", err) } - return nil } @@ -195,7 +188,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("servercow: failed to delete TXT records: %w", err) } - return nil } @@ -225,7 +217,8 @@ func getAuthZone(domain string) (string, error) { return "", fmt.Errorf("could not find zone: %w", err) } - return dns01.UnFqdn(authZone), nil + zoneName := dns01.UnFqdn(authZone) + return zoneName, nil } func findRecords(records []internal.Record, name string) *internal.Record { diff --git a/providers/dns/servercow/servercow.toml b/providers/dns/servercow/servercow.toml index 5cbacbb88..670ca6b14 100644 --- a/providers/dns/servercow/servercow.toml +++ b/providers/dns/servercow/servercow.toml @@ -7,7 +7,7 @@ Since = "v3.4.0" Example = ''' SERVERCOW_USERNAME=xxxxxxxx \ SERVERCOW_PASSWORD=xxxxxxxx \ -lego --dns servercow -d '*.example.com' -d example.com run +lego --email you@example.com --dns servercow --domains my.example.org run ''' [Configuration] @@ -15,10 +15,10 @@ lego --dns servercow -d '*.example.com' -d example.com run SERVERCOW_USERNAME = "API username" SERVERCOW_PASSWORD = "API password" [Configuration.Additional] - SERVERCOW_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - SERVERCOW_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - SERVERCOW_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - SERVERCOW_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + SERVERCOW_POLLING_INTERVAL = "Time between DNS propagation check" + SERVERCOW_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + SERVERCOW_TTL = "The TTL of the TXT record used for the DNS challenge" + SERVERCOW_HTTP_TIMEOUT = "API request timeout" [Links] - API = "https://wiki.servercow.de/en/domains/dns_api/api-syntax/" + API = "https://cp.servercow.de/client/plugin/support_manager/knowledgebase/view/34/dns-api-v1/7/" diff --git a/providers/dns/servercow/servercow_test.go b/providers/dns/servercow/servercow_test.go index f2328fe1a..1c3facad9 100644 --- a/providers/dns/servercow/servercow_test.go +++ b/providers/dns/servercow/servercow_test.go @@ -57,7 +57,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -130,7 +129,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -144,7 +142,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/shellrent/internal/client.go b/providers/dns/shellrent/internal/client.go index a70ff5452..a361ccf1d 100644 --- a/providers/dns/shellrent/internal/client.go +++ b/providers/dns/shellrent/internal/client.go @@ -29,7 +29,7 @@ type Client struct { } // NewClient Creates a new Client. -func NewClient(username, token string) *Client { +func NewClient(username string, token string) *Client { baseURL, _ := url.Parse(defaultBaseURL) return &Client{ @@ -42,7 +42,7 @@ func NewClient(username, token string) *Client { // ListServices lists service IDs. // https://api.shellrent.com/elenco-dei-servizi-acquistati -func (c *Client) ListServices(ctx context.Context) ([]int, error) { +func (c Client) ListServices(ctx context.Context) ([]int, error) { endpoint := c.baseURL.JoinPath("purchase") req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) @@ -72,7 +72,7 @@ func (c *Client) ListServices(ctx context.Context) ([]int, error) { // GetServiceDetails gets service details. // https://api.shellrent.com/dettagli-servizio-acquistato -func (c *Client) GetServiceDetails(ctx context.Context, serviceID int) (*ServiceDetails, error) { +func (c Client) GetServiceDetails(ctx context.Context, serviceID int) (*ServiceDetails, error) { endpoint := c.baseURL.JoinPath("purchase", "details", strconv.Itoa(serviceID)) req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) @@ -96,7 +96,7 @@ func (c *Client) GetServiceDetails(ctx context.Context, serviceID int) (*Service // GetDomainDetails gets domain details. // https://api.shellrent.com/dettagli-dominio -func (c *Client) GetDomainDetails(ctx context.Context, domainID int) (*DomainDetails, error) { +func (c Client) GetDomainDetails(ctx context.Context, domainID int) (*DomainDetails, error) { endpoint := c.baseURL.JoinPath("domain", "details", strconv.Itoa(domainID)) req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) @@ -114,13 +114,12 @@ func (c *Client) GetDomainDetails(ctx context.Context, domainID int) (*DomainDet if result.Code != 0 { return nil, result.Base } - return result.Data, nil } // CreateRecord created a record. // https://api.shellrent.com/creazione-record-dns-di-un-dominio -func (c *Client) CreateRecord(ctx context.Context, domainID int, record Record) (int, error) { +func (c Client) CreateRecord(ctx context.Context, domainID int, record Record) (int, error) { endpoint := c.baseURL.JoinPath("dns_record", "store", strconv.Itoa(domainID)) req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) @@ -138,13 +137,12 @@ func (c *Client) CreateRecord(ctx context.Context, domainID int, record Record) if result.Code != 0 { return 0, result.Base } - return result.Data.ID.Value(), nil } // DeleteRecord deletes a record. // https://api.shellrent.com/eliminazione-record-dns-di-un-dominio -func (c *Client) DeleteRecord(ctx context.Context, domainID, recordID int) error { +func (c Client) DeleteRecord(ctx context.Context, domainID int, recordID int) error { endpoint := c.baseURL.JoinPath("dns_record", "remove", strconv.Itoa(domainID), strconv.Itoa(recordID)) req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) @@ -166,7 +164,7 @@ func (c *Client) DeleteRecord(ctx context.Context, domainID, recordID int) error return nil } -func (c *Client) do(req *http.Request, result any) error { +func (c Client) do(req *http.Request, result any) error { req.Header.Set(authorizationHeader, c.username+"."+c.token) resp, err := c.HTTPClient.Do(req) @@ -221,7 +219,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) var response Base - err := json.Unmarshal(raw, &response) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/shellrent/internal/client_test.go b/providers/dns/shellrent/internal/client_test.go index 7047ce835..0fe77c6fc 100644 --- a/providers/dns/shellrent/internal/client_test.go +++ b/providers/dns/shellrent/internal/client_test.go @@ -1,35 +1,71 @@ package internal import ( + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("user", "secret") - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) +func setupTest(t *testing.T, method, pattern string, status int, file string) *Client { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("user.secret")) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest) + return + } + + auth := req.Header.Get(authorizationHeader) + if auth != "user.secret" { + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + if file == "" { + rw.WriteHeader(status) + return + } + + open, err := os.Open(filepath.Join("fixtures", file)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = open.Close() }() + + rw.WriteHeader(status) + _, err = io.Copy(rw, open) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + client := NewClient("user", "secret") + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client } func TestClient_ListServices(t *testing.T) { - client := mockBuilder(). - Route("GET /purchase", servermock.ResponseFromFixture("purchase.json")). - Build(t) + client := setupTest(t, http.MethodGet, "/purchase", http.StatusOK, "purchase.json") - services, err := client.ListServices(t.Context()) + services, err := client.ListServices(context.Background()) require.NoError(t, err) expected := []int{2018, 10039, 10128} @@ -38,31 +74,23 @@ func TestClient_ListServices(t *testing.T) { } func TestClient_ListServices_error(t *testing.T) { - client := mockBuilder(). - Route("GET /purchase", servermock.ResponseFromFixture("error.json")). - Build(t) + client := setupTest(t, http.MethodGet, "/purchase", http.StatusOK, "error.json") - _, err := client.ListServices(t.Context()) + _, err := client.ListServices(context.Background()) require.EqualError(t, err, "code 2: Token di autorizzazione non valido") } func TestClient_ListServices_error_status(t *testing.T) { - client := mockBuilder(). - Route("GET /purchase", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client := setupTest(t, http.MethodGet, "/purchase", http.StatusUnauthorized, "error.json") - _, err := client.ListServices(t.Context()) + _, err := client.ListServices(context.Background()) require.EqualError(t, err, "code 2: Token di autorizzazione non valido") } func TestClient_GetServiceDetails(t *testing.T) { - client := mockBuilder(). - Route("GET /purchase/details/123", servermock.ResponseFromFixture("purchase-details.json")). - Build(t) + client := setupTest(t, http.MethodGet, "/purchase/details/123", http.StatusOK, "purchase-details.json") - services, err := client.GetServiceDetails(t.Context(), 123) + services, err := client.GetServiceDetails(context.Background(), 123) require.NoError(t, err) expected := &ServiceDetails{ID: 123, Name: "example", DomainID: 456} @@ -71,31 +99,23 @@ func TestClient_GetServiceDetails(t *testing.T) { } func TestClient_GetServiceDetails_error(t *testing.T) { - client := mockBuilder(). - Route("GET /purchase/details/123", servermock.ResponseFromFixture("error.json")). - Build(t) + client := setupTest(t, http.MethodGet, "/purchase/details/123", http.StatusOK, "error.json") - _, err := client.GetServiceDetails(t.Context(), 123) + _, err := client.GetServiceDetails(context.Background(), 123) require.EqualError(t, err, "code 2: Token di autorizzazione non valido") } func TestClient_GetServiceDetails_error_status(t *testing.T) { - client := mockBuilder(). - Route("GET /purchase/details/123", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client := setupTest(t, http.MethodGet, "/purchase/details/123", http.StatusUnauthorized, "error.json") - _, err := client.GetServiceDetails(t.Context(), 123) + _, err := client.GetServiceDetails(context.Background(), 123) require.EqualError(t, err, "code 2: Token di autorizzazione non valido") } func TestClient_GetDomainDetails(t *testing.T) { - client := mockBuilder(). - Route("GET /domain/details/123", servermock.ResponseFromFixture("domain-details.json")). - Build(t) + client := setupTest(t, http.MethodGet, "/domain/details/123", http.StatusOK, "domain-details.json") - services, err := client.GetDomainDetails(t.Context(), 123) + services, err := client.GetDomainDetails(context.Background(), 123) require.NoError(t, err) expected := &DomainDetails{ID: 123, DomainName: "example.com", DomainNameASCII: "example.com"} @@ -104,31 +124,23 @@ func TestClient_GetDomainDetails(t *testing.T) { } func TestClient_GetDomainDetails_error(t *testing.T) { - client := mockBuilder(). - Route("GET /domain/details/123", servermock.ResponseFromFixture("error.json")). - Build(t) + client := setupTest(t, http.MethodGet, "/domain/details/123", http.StatusOK, "error.json") - _, err := client.GetDomainDetails(t.Context(), 123) + _, err := client.GetDomainDetails(context.Background(), 123) require.EqualError(t, err, "code 2: Token di autorizzazione non valido") } func TestClient_GetDomainDetails_error_status(t *testing.T) { - client := mockBuilder(). - Route("GET /domain/details/123", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client := setupTest(t, http.MethodGet, "/domain/details/123", http.StatusUnauthorized, "error.json") - _, err := client.GetDomainDetails(t.Context(), 123) + _, err := client.GetDomainDetails(context.Background(), 123) require.EqualError(t, err, "code 2: Token di autorizzazione non valido") } func TestClient_CreateRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /dns_record/store/123", servermock.ResponseFromFixture("dns_record-store.json")). - Build(t) + client := setupTest(t, http.MethodPost, "/dns_record/store/123", http.StatusOK, "dns_record-store.json") - services, err := client.CreateRecord(t.Context(), 123, Record{}) + services, err := client.CreateRecord(context.Background(), 123, Record{}) require.NoError(t, err) expected := 2255674 @@ -137,51 +149,37 @@ func TestClient_CreateRecord(t *testing.T) { } func TestClient_CreateRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /dns_record/store/123", servermock.ResponseFromFixture("error.json")). - Build(t) + client := setupTest(t, http.MethodPost, "/dns_record/store/123", http.StatusOK, "error.json") - _, err := client.CreateRecord(t.Context(), 123, Record{}) + _, err := client.CreateRecord(context.Background(), 123, Record{}) require.EqualError(t, err, "code 2: Token di autorizzazione non valido") } func TestClient_CreateRecord_error_status(t *testing.T) { - client := mockBuilder(). - Route("POST /dns_record/store/123", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client := setupTest(t, http.MethodPost, "/dns_record/store/123", http.StatusUnauthorized, "error.json") - _, err := client.CreateRecord(t.Context(), 123, Record{}) + _, err := client.CreateRecord(context.Background(), 123, Record{}) require.EqualError(t, err, "code 2: Token di autorizzazione non valido") } func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /dns_record/remove/123/456", servermock.ResponseFromFixture("dns_record-remove.json")). - Build(t) + client := setupTest(t, http.MethodDelete, "/dns_record/remove/123/456", http.StatusOK, "dns_record-remove.json") - err := client.DeleteRecord(t.Context(), 123, 456) + err := client.DeleteRecord(context.Background(), 123, 456) require.NoError(t, err) } func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /dns_record/remove/123/456", servermock.ResponseFromFixture("error.json")). - Build(t) + client := setupTest(t, http.MethodDelete, "/dns_record/remove/123/456", http.StatusOK, "error.json") - err := client.DeleteRecord(t.Context(), 123, 456) + err := client.DeleteRecord(context.Background(), 123, 456) require.EqualError(t, err, "code 2: Token di autorizzazione non valido") } func TestClient_DeleteRecord_error_status(t *testing.T) { - client := mockBuilder(). - Route("DELETE /dns_record/remove/123/456", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client := setupTest(t, http.MethodDelete, "/dns_record/remove/123/456", http.StatusUnauthorized, "error.json") - err := client.DeleteRecord(t.Context(), 123, 456) + err := client.DeleteRecord(context.Background(), 123, 456) require.EqualError(t, err, "code 2: Token di autorizzazione non valido") } diff --git a/providers/dns/shellrent/internal/types.go b/providers/dns/shellrent/internal/types.go index 6bdd82330..a27b06347 100644 --- a/providers/dns/shellrent/internal/types.go +++ b/providers/dns/shellrent/internal/types.go @@ -7,7 +7,6 @@ import ( type Response[T any] struct { Base - Data T `json:"data"` } @@ -58,7 +57,6 @@ func (m *IntOrString) UnmarshalJSON(data []byte) error { raw := string(data) if data[0] == '"' { var err error - raw, err = strconv.Unquote(string(data)) if err != nil { return err diff --git a/providers/dns/shellrent/shellrent.go b/providers/dns/shellrent/shellrent.go index 0cd33e19a..d1054b378 100644 --- a/providers/dns/shellrent/shellrent.go +++ b/providers/dns/shellrent/shellrent.go @@ -9,10 +9,8 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/shellrent/internal" ) @@ -31,8 +29,6 @@ const ( const defaultTTL = 3600 -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - type reqKey struct { domainID int recordID int @@ -104,8 +100,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, client: client, @@ -126,7 +120,9 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { zone, err := d.findZone(ctx, dns01.UnFqdn(info.EffectiveFQDN)) if err != nil { - return fmt.Errorf("shellrent: could not find zone for domain %q: %w", domain, err) + if err != nil { + return fmt.Errorf("shellrent: could not find zone for domain %q: %w", domain, err) + } } subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.DomainName) @@ -162,7 +158,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { d.recordIDsMu.Lock() key, ok := d.recordIDs[token] d.recordIDsMu.Unlock() - if !ok { return fmt.Errorf("shellrent: unknown request key for '%s' '%s'", info.EffectiveFQDN, token) } @@ -172,10 +167,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("shellrent: delete record: %w", err) } - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - return nil } diff --git a/providers/dns/shellrent/shellrent.toml b/providers/dns/shellrent/shellrent.toml index 05b6517fc..5c63db19f 100644 --- a/providers/dns/shellrent/shellrent.toml +++ b/providers/dns/shellrent/shellrent.toml @@ -7,7 +7,7 @@ Since = "v4.16.0" Example = ''' SHELLRENT_USERNAME=xxxx \ SHELLRENT_TOKEN=yyyy \ -lego --dns shellrent -d '*.example.com' -d example.com run +lego --email you@example.com --dns shellrent --domains my.example.org run ''' [Configuration] @@ -15,10 +15,10 @@ lego --dns shellrent -d '*.example.com' -d example.com run SHELLRENT_USERNAME = "Username" SHELLRENT_TOKEN = "Token" [Configuration.Additional] - SHELLRENT_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" - SHELLRENT_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 300)" - SHELLRENT_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)" - SHELLRENT_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + SHELLRENT_POLLING_INTERVAL = "Time between DNS propagation check" + SHELLRENT_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + SHELLRENT_TTL = "The TTL of the TXT record used for the DNS challenge" + SHELLRENT_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://api.shellrent.com/section/api2" diff --git a/providers/dns/shellrent/shellrent_test.go b/providers/dns/shellrent/shellrent_test.go index 8c4e3f6bf..e5d529917 100644 --- a/providers/dns/shellrent/shellrent_test.go +++ b/providers/dns/shellrent/shellrent_test.go @@ -47,7 +47,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -118,7 +117,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -132,7 +130,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/simply/internal/client.go b/providers/dns/simply/internal/client.go index 0c0655463..b57bf2102 100644 --- a/providers/dns/simply/internal/client.go +++ b/providers/dns/simply/internal/client.go @@ -16,7 +16,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/internal/errutils" ) -const defaultBaseURL = "https://api.simply.com/2/" +const defaultBaseURL = "https://api.simply.com/1/" // Client is a Simply.com API client. type Client struct { @@ -28,7 +28,7 @@ type Client struct { } // NewClient creates a new Client. -func NewClient(accountName, apiKey string) (*Client, error) { +func NewClient(accountName string, apiKey string) (*Client, error) { if accountName == "" { return nil, errors.New("credentials missing: accountName") } @@ -60,7 +60,6 @@ func (c *Client) GetRecords(ctx context.Context, zoneName string) ([]Record, err } result := &apiResponse[[]Record, json.RawMessage]{} - err = c.do(req, result) if err != nil { return nil, err @@ -79,7 +78,6 @@ func (c *Client) AddRecord(ctx context.Context, zoneName string, record Record) } result := &apiResponse[json.RawMessage, recordHeader]{} - err = c.do(req, result) if err != nil { return 0, err @@ -112,13 +110,11 @@ func (c *Client) DeleteRecord(ctx context.Context, zoneName string, id int64) er return c.do(req, &apiResponse[json.RawMessage, json.RawMessage]{}) } -func (c *Client) createEndpoint(zoneName, uri string) *url.URL { - return c.baseURL.JoinPath("my", "products", zoneName, "dns", "records", strings.TrimSuffix(uri, "/")) +func (c *Client) createEndpoint(zoneName string, uri string) *url.URL { + return c.baseURL.JoinPath(c.accountName, c.apiKey, "my", "products", zoneName, "dns", "records", strings.TrimSuffix(uri, "/")) } func (c *Client) do(req *http.Request, result Response) error { - req.SetBasicAuth(c.accountName, c.apiKey) - resp, err := c.HTTPClient.Do(req) if err != nil { return errutils.NewHTTPDoError(req, err) diff --git a/providers/dns/simply/internal/client_test.go b/providers/dns/simply/internal/client_test.go index b0bdac6b3..c9b97e94c 100644 --- a/providers/dns/simply/internal/client_test.go +++ b/providers/dns/simply/internal/client_test.go @@ -1,40 +1,27 @@ package internal import ( + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" + "path" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("accountname", "apikey") - if err != nil { - return nil, err - } - - client.baseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithBasicAuth("accountname", "apikey")) -} - func TestClient_GetRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /my/products/azone01/dns/records", - servermock.ResponseFromFixture("get_records.json")). - Build(t) + client, mux := setupTest(t) - records, err := client.GetRecords(t.Context(), "azone01") + mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records", mockHandler(http.MethodGet, http.StatusOK, "get_records.json")) + + records, err := client.GetRecords(context.Background(), "azone01") require.NoError(t, err) expected := []Record{ @@ -76,23 +63,20 @@ func TestClient_GetRecords(t *testing.T) { } func TestClient_GetRecords_error(t *testing.T) { - client := mockBuilder(). - Route("GET /my/products/azone01/dns/records", - servermock.ResponseFromFixture("bad_auth_error.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) + client, mux := setupTest(t) - records, err := client.GetRecords(t.Context(), "azone01") + mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records", mockHandler(http.MethodGet, http.StatusBadRequest, "bad_auth_error.json")) + + records, err := client.GetRecords(context.Background(), "azone01") require.Error(t, err) assert.Nil(t, records) } func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /my/products/azone01/dns/records", - servermock.ResponseFromFixture("add_record.json")). - Build(t) + client, mux := setupTest(t) + + mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records", mockHandler(http.MethodPost, http.StatusOK, "add_record.json")) record := Record{ Name: "arecord01", @@ -102,18 +86,16 @@ func TestClient_AddRecord(t *testing.T) { Priority: 0, } - recordID, err := client.AddRecord(t.Context(), "azone01", record) + recordID, err := client.AddRecord(context.Background(), "azone01", record) require.NoError(t, err) assert.EqualValues(t, 123456789, recordID) } func TestClient_AddRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /my/products/azone01/dns/records", - servermock.ResponseFromFixture("bad_zone_error.json"). - WithStatusCode(http.StatusNotFound)). - Build(t) + client, mux := setupTest(t) + + mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records", mockHandler(http.MethodPost, http.StatusNotFound, "bad_zone_error.json")) record := Record{ Name: "arecord01", @@ -123,17 +105,16 @@ func TestClient_AddRecord_error(t *testing.T) { Priority: 0, } - recordID, err := client.AddRecord(t.Context(), "azone01", record) + recordID, err := client.AddRecord(context.Background(), "azone01", record) require.Error(t, err) assert.Zero(t, recordID) } func TestClient_EditRecord(t *testing.T) { - client := mockBuilder(). - Route("PUT /my/products/azone01/dns/records/123456789", - servermock.ResponseFromFixture("success.json")). - Build(t) + client, mux := setupTest(t) + + mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records/123456789", mockHandler(http.MethodPut, http.StatusOK, "success.json")) record := Record{ Name: "arecord01", @@ -143,16 +124,14 @@ func TestClient_EditRecord(t *testing.T) { Priority: 0, } - err := client.EditRecord(t.Context(), "azone01", 123456789, record) + err := client.EditRecord(context.Background(), "azone01", 123456789, record) require.NoError(t, err) } func TestClient_EditRecord_error(t *testing.T) { - client := mockBuilder(). - Route("PUT /my/products/azone01/dns/records/123456789", - servermock.ResponseFromFixture("invalid_record_id.json"). - WithStatusCode(http.StatusNotFound)). - Build(t) + client, mux := setupTest(t) + + mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records/123456789", mockHandler(http.MethodPut, http.StatusNotFound, "invalid_record_id.json")) record := Record{ Name: "arecord01", @@ -162,27 +141,68 @@ func TestClient_EditRecord_error(t *testing.T) { Priority: 0, } - err := client.EditRecord(t.Context(), "azone01", 123456789, record) + err := client.EditRecord(context.Background(), "azone01", 123456789, record) require.Error(t, err) } func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /my/products/azone01/dns/records/123456789", - servermock.ResponseFromFixture("success.json")). - Build(t) + client, mux := setupTest(t) - err := client.DeleteRecord(t.Context(), "azone01", 123456789) + mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records/123456789", mockHandler(http.MethodDelete, http.StatusOK, "success.json")) + + err := client.DeleteRecord(context.Background(), "azone01", 123456789) require.NoError(t, err) } func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /my/products/azone01/dns/records/123456789", - servermock.ResponseFromFixture("invalid_record_id.json"). - WithStatusCode(http.StatusNotFound)). - Build(t) + client, mux := setupTest(t) - err := client.DeleteRecord(t.Context(), "azone01", 123456789) + mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records/123456789", mockHandler(http.MethodDelete, http.StatusNotFound, "invalid_record_id.json")) + + err := client.DeleteRecord(context.Background(), "azone01", 123456789) require.Error(t, err) } + +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client, err := NewClient("accountname", "apikey") + require.NoError(t, err) + + client.baseURL, _ = url.Parse(server.URL) + + return client, mux +} + +func mockHandler(method string, statusCode int, filename string) func(http.ResponseWriter, *http.Request) { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + if filename == "" { + rw.WriteHeader(statusCode) + return + } + + file, err := os.Open(filepath.FromSlash(path.Join("./fixtures", filename))) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + rw.WriteHeader(statusCode) + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + } +} diff --git a/providers/dns/simply/simply.go b/providers/dns/simply/simply.go index fc3afd310..2433c4e06 100644 --- a/providers/dns/simply/simply.go +++ b/providers/dns/simply/simply.go @@ -9,10 +9,8 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/simply/internal" ) @@ -29,8 +27,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { AccountName string @@ -100,8 +96,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, client: client, @@ -165,7 +159,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() - if !ok { return fmt.Errorf("simply: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) } diff --git a/providers/dns/simply/simply.toml b/providers/dns/simply/simply.toml index a838e245a..4b6c0cd02 100644 --- a/providers/dns/simply/simply.toml +++ b/providers/dns/simply/simply.toml @@ -7,7 +7,7 @@ Since = "v4.4.0" Example = ''' SIMPLY_ACCOUNT_NAME=xxxxxx \ SIMPLY_API_KEY=yyyyyy \ -lego --dns simply -d '*.example.com' -d example.com run +lego --email you@example.com --dns simply --domains my.example.org run ''' [Configuration] @@ -15,11 +15,10 @@ lego --dns simply -d '*.example.com' -d example.com run SIMPLY_ACCOUNT_NAME = "Account name" SIMPLY_API_KEY = "API key" [Configuration.Additional] - SIMPLY_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" - SIMPLY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 300)" - SIMPLY_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - SIMPLY_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + SIMPLY_POLLING_INTERVAL = "Time between DNS propagation check" + SIMPLY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + SIMPLY_TTL = "The TTL of the TXT record used for the DNS challenge" + SIMPLY_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.simply.com/en/docs/api/" - Spec = "https://generator.swagger.io/?url=https://api.simply.com/2/openapi.json#/" diff --git a/providers/dns/simply/simply_test.go b/providers/dns/simply/simply_test.go index e6de60d43..ace8e0b72 100644 --- a/providers/dns/simply/simply_test.go +++ b/providers/dns/simply/simply_test.go @@ -53,7 +53,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -122,7 +121,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -136,7 +134,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/sonic/internal/client.go b/providers/dns/sonic/internal/client.go index cf8f7f067..aac85c636 100644 --- a/providers/dns/sonic/internal/client.go +++ b/providers/dns/sonic/internal/client.go @@ -42,7 +42,7 @@ func NewClient(userID, apiKey string) (*Client, error) { // SetRecord creates or updates a TXT records. // Sonic does not provide a delete record API endpoint. // https://public-api.sonic.net/dyndns#updating_or_adding_host_records -func (c *Client) SetRecord(ctx context.Context, hostname, value string, ttl int) error { +func (c *Client) SetRecord(ctx context.Context, hostname string, value string, ttl int) error { payload := &Record{ UserID: c.userID, APIKey: c.apiKey, @@ -83,7 +83,6 @@ func (c *Client) SetRecord(ctx context.Context, hostname, value string, ttl int) } r := APIResponse{} - err = json.Unmarshal(raw, &r) if err != nil { return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) diff --git a/providers/dns/sonic/internal/client_test.go b/providers/dns/sonic/internal/client_test.go index 751ccee8f..ac711387e 100644 --- a/providers/dns/sonic/internal/client_test.go +++ b/providers/dns/sonic/internal/client_test.go @@ -1,23 +1,32 @@ package internal import ( + "context" + "fmt" + "net/http" "net/http/httptest" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/require" ) -func setupClient(server *httptest.Server) (*Client, error) { +func setupTest(t *testing.T, body string) *Client { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc("/host", func(rw http.ResponseWriter, req *http.Request) { + _, _ = fmt.Fprintln(rw, body) + }) + client, err := NewClient("foo", "secret") - if err != nil { - return nil, err - } + require.NoError(t, err) client.baseURL = server.URL - client.HTTPClient = server.Client() - return client, nil + return client } func TestClient_SetRecord(t *testing.T) { @@ -42,13 +51,9 @@ func TestClient_SetRecord(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - client := servermock.NewBuilder[*Client](setupClient, servermock.CheckHeader().WithJSONHeaders()). - Route("PUT /host", - servermock.RawStringResponse(test.response), - servermock.CheckRequestJSONBody(`{"userid":"foo","apikey":"secret","hostname":"example.com","value":"txttxttxt","ttl":10,"type":"TXT"}`)). - Build(t) + client := setupTest(t, test.response) - err := client.SetRecord(t.Context(), "example.com", "txttxttxt", 10) + err := client.SetRecord(context.Background(), "example.com", "txttxttxt", 10) test.assert(t, err) }) } diff --git a/providers/dns/sonic/sonic.go b/providers/dns/sonic/sonic.go index 5bda2b533..19c5769b3 100644 --- a/providers/dns/sonic/sonic.go +++ b/providers/dns/sonic/sonic.go @@ -8,10 +8,8 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/sonic/internal" ) @@ -29,8 +27,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { UserID string @@ -92,8 +88,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{client: client, config: config}, nil } diff --git a/providers/dns/sonic/sonic.toml b/providers/dns/sonic/sonic.toml index cb501e923..c4ba74dd5 100644 --- a/providers/dns/sonic/sonic.toml +++ b/providers/dns/sonic/sonic.toml @@ -7,7 +7,7 @@ Since = "v4.4.0" Example = ''' SONIC_USER_ID=12345 \ SONIC_API_KEY=4d6fbf2f9ab0fa11697470918d37625851fc0c51 \ -lego --dns sonic -d '*.example.com' -d example.com run +lego --email you@example.com --dns sonic --domains my.example.org run ''' Additional = ''' @@ -34,11 +34,11 @@ Hostname should be the toplevel domain managed e.g. `example.com` not `www.examp SONIC_USER_ID = "User ID" SONIC_API_KEY = "API Key" [Configuration.Additional] - SONIC_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - SONIC_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - SONIC_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - SONIC_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 60)" - SONIC_HTTP_TIMEOUT = "API request timeout in seconds (Default: 10)" + SONIC_POLLING_INTERVAL = "Time between DNS propagation check" + SONIC_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + SONIC_TTL = "The TTL of the TXT record used for the DNS challenge" + SONIC_HTTP_TIMEOUT = "API request timeout" + SONIC_SEQUENCE_INTERVAL = "Time between sequential requests" [Links] API = "https://public-api.sonic.net/dyndns/" diff --git a/providers/dns/sonic/sonic_test.go b/providers/dns/sonic/sonic_test.go index 7dc7fc586..f9087f8e3 100644 --- a/providers/dns/sonic/sonic_test.go +++ b/providers/dns/sonic/sonic_test.go @@ -49,7 +49,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -120,7 +119,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -134,7 +132,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/spaceship/internal/client.go b/providers/dns/spaceship/internal/client.go deleted file mode 100644 index e690fa467..000000000 --- a/providers/dns/spaceship/internal/client.go +++ /dev/null @@ -1,161 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" -) - -const defaultBaseURL = "https://spaceship.dev/api/v1/" - -// Client the Spaceship API client. -type Client struct { - apiKey string - apiSecret string - - baseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(apiKey, apiSecret string) (*Client, error) { - if apiKey == "" || apiSecret == "" { - return nil, errors.New("credentials missing") - } - - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - apiKey: apiKey, - apiSecret: apiSecret, - baseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -func (c *Client) do(req *http.Request, result any) error { - req.Header.Add("X-Api-Secret", c.apiSecret) - req.Header.Add("X-Api-Key", c.apiKey) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func (c *Client) AddRecord(ctx context.Context, domain string, record Record) error { - endpoint := c.baseURL.JoinPath("dns", "records", domain) - - req, err := newJSONRequest(ctx, http.MethodPut, endpoint, Foo{Items: []Record{record}}) - if err != nil { - return err - } - - err = c.do(req, nil) - if err != nil { - return err - } - - return nil -} - -func (c *Client) DeleteRecord(ctx context.Context, domain string, record Record) error { - endpoint := c.baseURL.JoinPath("dns", "records", domain) - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, []Record{record}) - if err != nil { - return err - } - - err = c.do(req, nil) - if err != nil { - return err - } - - return nil -} - -func (c *Client) GetRecords(ctx context.Context, domain string) ([]Record, error) { - endpoint := c.baseURL.JoinPath("dns", "records", domain) - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var result GetRecordsResponse - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return result.Items, nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIError - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return &errAPI -} diff --git a/providers/dns/spaceship/internal/client_test.go b/providers/dns/spaceship/internal/client_test.go deleted file mode 100644 index f32843652..000000000 --- a/providers/dns/spaceship/internal/client_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("key", "secret") - if err != nil { - return nil, err - } - - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - With("X-Api-Key", "key"). - With("X-Api-Secret", "secret"), - ) -} - -func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("PUT /dns/records/example.com", nil, - servermock.CheckRequestJSONBody(`{"items":[{"type":"TXT","name":"@","ttl":60}]}`)). - Build(t) - - record := Record{ - Type: "TXT", - Name: "@", - TTL: 60, - } - - err := client.AddRecord(t.Context(), "example.com", record) - require.NoError(t, err) -} - -func TestClient_AddRecord_error(t *testing.T) { - client := mockBuilder(). - Route("PUT /dns/records/example.com", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnprocessableEntity)). - Build(t) - - record := Record{ - Type: "TXT", - Name: "@", - TTL: 60, - } - - err := client.AddRecord(t.Context(), "example.com", record) - require.EqualError(t, err, "^$, name: The domain name contains invalid characters") -} - -func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /dns/records/example.com", nil, - servermock.CheckRequestJSONBody(`[{"type":"TXT","name":"@","ttl":60}]`)). - Build(t) - - record := Record{ - Type: "TXT", - Name: "@", - TTL: 60, - } - - err := client.DeleteRecord(t.Context(), "example.com", record) - require.NoError(t, err) -} - -func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /dns/records/example.com", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnprocessableEntity)). - Build(t) - - record := Record{ - Type: "TXT", - Name: "@", - TTL: 60, - } - - err := client.DeleteRecord(t.Context(), "example.com", record) - require.EqualError(t, err, "^$, name: The domain name contains invalid characters") -} - -func TestClient_GetRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/records/example.com", - servermock.ResponseFromFixture("get-records.json")). - Build(t) - - records, err := client.GetRecords(t.Context(), "example.com") - require.NoError(t, err) - - expected := []Record{ - {Type: "A", Name: "@", TTL: 3600}, - } - - assert.Equal(t, expected, records) -} - -func TestClient_GetRecords_error(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/records/example.com", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnprocessableEntity)). - Build(t) - - _, err := client.GetRecords(t.Context(), "example.com") - require.EqualError(t, err, "^$, name: The domain name contains invalid characters") -} diff --git a/providers/dns/spaceship/internal/fixtures/error.json b/providers/dns/spaceship/internal/fixtures/error.json deleted file mode 100644 index facf97e58..000000000 --- a/providers/dns/spaceship/internal/fixtures/error.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "detail": "^$", - "data": [ - { - "field": "name", - "details": "The domain name contains invalid characters" - } - ] -} diff --git a/providers/dns/spaceship/internal/fixtures/get-records.json b/providers/dns/spaceship/internal/fixtures/get-records.json deleted file mode 100644 index cea2a895a..000000000 --- a/providers/dns/spaceship/internal/fixtures/get-records.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "items": [ - { - "type": "A", - "name": "@", - "ttl": 3600, - "group": { - "type": "custom" - } - } - ], - "total": 100 -} diff --git a/providers/dns/spaceship/internal/types.go b/providers/dns/spaceship/internal/types.go deleted file mode 100644 index bd318bb87..000000000 --- a/providers/dns/spaceship/internal/types.go +++ /dev/null @@ -1,47 +0,0 @@ -package internal - -import ( - "fmt" - "strings" -) - -type APIError struct { - Detail string `json:"detail"` - Data []struct { - Field string `json:"field"` - Details string `json:"details"` - } `json:"data"` -} - -func (a *APIError) Error() string { - msg := []string{a.Detail} - - for _, datum := range a.Data { - msg = append(msg, fmt.Sprintf("%s: %s", datum.Field, datum.Details)) - } - - return strings.Join(msg, ", ") -} - -type Foo struct { - Force bool `json:"force,omitempty"` - Items []Record `json:"items,omitempty"` -} - -type Record struct { - Type string `json:"type,omitempty"` - Name string `json:"name,omitempty"` - Value string `json:"value,omitempty"` - Address string `json:"address,omitempty"` - Nameserver string `json:"nameserver,omitempty"` - AliasName string `json:"aliasName,omitempty"` - Pointer string `json:"pointer,omitempty"` - CName string `json:"cname,omitempty"` - Exchange string `json:"exchange,omitempty"` - TTL int `json:"ttl,omitempty"` -} - -type GetRecordsResponse struct { - Items []Record `json:"items"` - Total int `json:"total"` -} diff --git a/providers/dns/spaceship/spaceship.go b/providers/dns/spaceship/spaceship.go deleted file mode 100644 index e34c584c5..000000000 --- a/providers/dns/spaceship/spaceship.go +++ /dev/null @@ -1,157 +0,0 @@ -// Package spaceship implements a DNS provider for solving the DNS-01 challenge using Spaceship. -package spaceship - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/spaceship/internal" -) - -// Environment variables names. -const ( - envNamespace = "SPACESHIP_" - - EnvAPIKey = envNamespace + "API_KEY" - EnvAPISecret = envNamespace + "API_SECRET" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIKey string - APISecret string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for Spaceship. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIKey, EnvAPISecret) - if err != nil { - return nil, fmt.Errorf("spaceship: %w", err) - } - - config := NewDefaultConfig() - config.APIKey = values[EnvAPIKey] - config.APISecret = values[EnvAPISecret] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Spaceship. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("spaceship: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.APIKey, config.APISecret) - if err != nil { - return nil, fmt.Errorf("spaceship: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("spaceship: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("spaceship: %w", err) - } - - record := internal.Record{ - Type: "TXT", - Name: subDomain, - Value: info.Value, - TTL: d.config.TTL, - } - - err = d.client.AddRecord(context.Background(), dns01.UnFqdn(authZone), record) - if err != nil { - return fmt.Errorf("spaceship: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("spaceship: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("spaceship: %w", err) - } - - record := internal.Record{ - Type: "TXT", - Name: subDomain, - Value: info.Value, - } - - err = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), record) - if err != nil { - return fmt.Errorf("spaceship: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/spaceship/spaceship.toml b/providers/dns/spaceship/spaceship.toml deleted file mode 100644 index e9abcd408..000000000 --- a/providers/dns/spaceship/spaceship.toml +++ /dev/null @@ -1,24 +0,0 @@ -Name = "Spaceship" -Description = '''''' -URL = "https://www.spaceship.com/" -Code = "spaceship" -Since = "v4.22.0" - -Example = ''' -SPACESHIP_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -SPACESHIP_API_SECRET="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns spaceship -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - SPACESHIP_API_KEY = "API key" - SPACESHIP_API_SECRET = "API secret" - [Configuration.Additional] - SPACESHIP_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - SPACESHIP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - SPACESHIP_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - SPACESHIP_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://docs.spaceship.dev/#tag/DNS-records" diff --git a/providers/dns/spaceship/spaceship_test.go b/providers/dns/spaceship/spaceship_test.go deleted file mode 100644 index d4eb37d88..000000000 --- a/providers/dns/spaceship/spaceship_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package spaceship - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAPIKey, EnvAPISecret).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAPIKey: "key", - EnvAPISecret: "secret", - }, - }, - { - desc: "missing API key", - envVars: map[string]string{ - EnvAPIKey: "", - EnvAPISecret: "secret", - }, - expected: "spaceship: some credentials information are missing: SPACESHIP_API_KEY", - }, - { - desc: "missing API secret", - envVars: map[string]string{ - EnvAPIKey: "key", - EnvAPISecret: "", - }, - expected: "spaceship: some credentials information are missing: SPACESHIP_API_SECRET", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "spaceship: some credentials information are missing: SPACESHIP_API_KEY,SPACESHIP_API_SECRET", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiKey string - apiSecret string - expected string - }{ - { - desc: "success", - apiKey: "key", - apiSecret: "secret", - }, - { - desc: "missing API key", - apiSecret: "secret", - expected: "spaceship: credentials missing", - }, - { - desc: "missing API secret", - apiKey: "key", - expected: "spaceship: credentials missing", - }, - { - desc: "missing credentials", - expected: "spaceship: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIKey = test.apiKey - config.APISecret = test.apiSecret - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/stackpath/internal/client.go b/providers/dns/stackpath/internal/client.go index 8a40a4093..bd11bf235 100644 --- a/providers/dns/stackpath/internal/client.go +++ b/providers/dns/stackpath/internal/client.go @@ -25,13 +25,13 @@ type Client struct { } // NewClient creates a new Client. -func NewClient(stackID string, hc *http.Client) *Client { +func NewClient(ctx context.Context, stackID, clientID, clientSecret string) *Client { baseURL, _ := url.Parse(defaultBaseURL) return &Client{ baseURL: baseURL, stackID: stackID, - httpClient: hc, + httpClient: createOAuthClient(ctx, clientID, clientSecret), } } @@ -55,7 +55,6 @@ func (c *Client) GetZones(ctx context.Context, domain string) (*Zone, error) { req.URL.RawQuery = query.Encode() var zones Zones - err = c.do(req, &zones) if err != nil { return nil, err @@ -83,7 +82,6 @@ func (c *Client) GetZoneRecords(ctx context.Context, name string, zone *Zone) ([ req.URL.RawQuery = query.Encode() var records Records - err = c.do(req, &records) if err != nil { return nil, err @@ -179,7 +177,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) errResp := &ErrorResponse{} - err := json.Unmarshal(raw, errResp) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/stackpath/internal/client_test.go b/providers/dns/stackpath/internal/client_test.go index baac84397..2de1d4761 100644 --- a/providers/dns/stackpath/internal/client_test.go +++ b/providers/dns/stackpath/internal/client_test.go @@ -1,38 +1,50 @@ package internal import ( + "context" "net/http" "net/http/httptest" "net/url" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("STACK_ID", server.Client()) +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() - client.baseURL, _ = url.Parse(server.URL + "/") + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(), - ) + client := NewClient(context.Background(), "STACK_ID", "CLIENT_ID", "CLIENT_SECRET") + client.httpClient = server.Client() + client.baseURL, _ = url.Parse(server.URL + "/") + + return client, mux } func TestClient_GetZoneRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /STACK_ID/zones/A/records", - servermock.ResponseFromFixture("get_zone_records.json"), - servermock.CheckQueryParameter().Strict(). - With("page_request.filter", "name='foo1' and type='TXT'")). - Build(t) + client, mux := setupTest(t) - records, err := client.GetZoneRecords(t.Context(), "foo1", &Zone{ID: "A", Domain: "test"}) + mux.HandleFunc("/STACK_ID/zones/A/records", func(w http.ResponseWriter, _ *http.Request) { + content := ` + { + "records": [ + {"id":"1","name":"foo1","type":"TXT","ttl":120,"data":"txtTXTtxt"}, + {"id":"2","name":"foo2","type":"TXT","ttl":121,"data":"TXTtxtTXT"} + ] + }` + + _, err := w.Write([]byte(content)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + records, err := client.GetZoneRecords(context.Background(), "foo1", &Zone{ID: "A", Domain: "test"}) require.NoError(t, err) expected := []Record{ @@ -44,30 +56,73 @@ func TestClient_GetZoneRecords(t *testing.T) { } func TestClient_GetZoneRecords_apiError(t *testing.T) { - client := mockBuilder(). - Route("GET /STACK_ID/zones/A/records", - servermock.RawStringResponse(` + client, mux := setupTest(t) + + mux.HandleFunc("/STACK_ID/zones/A/records", func(w http.ResponseWriter, _ *http.Request) { + content := ` { "code": 401, "error": "an unauthorized request is attempted." -}`).WithStatusCode(http.StatusUnauthorized)). - Build(t) +}` - _, err := client.GetZoneRecords(t.Context(), "foo1", &Zone{ID: "A", Domain: "test"}) + w.WriteHeader(http.StatusUnauthorized) + _, err := w.Write([]byte(content)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + _, err := client.GetZoneRecords(context.Background(), "foo1", &Zone{ID: "A", Domain: "test"}) expected := &ErrorResponse{Code: 401, Message: "an unauthorized request is attempted."} assert.Equal(t, expected, err) } func TestClient_GetZones(t *testing.T) { - client := mockBuilder(). - Route("GET /STACK_ID/zones", - servermock.ResponseFromFixture("get_zones.json"), - servermock.CheckQueryParameter().Strict(). - With("page_request.filter", "domain='foo.com'")). - Build(t) + client, mux := setupTest(t) - zone, err := client.GetZones(t.Context(), "sub.foo.com") + mux.HandleFunc("/STACK_ID/zones", func(w http.ResponseWriter, _ *http.Request) { + content := ` +{ + "pageInfo": { + "totalCount": "5", + "hasPreviousPage": false, + "hasNextPage": false, + "startCursor": "1", + "endCursor": "1" + }, + "zones": [ + { + "stackId": "my_stack", + "accountId": "my_account", + "id": "A", + "domain": "foo.com", + "version": "1", + "labels": { + "property1": "val1", + "property2": "val2" + }, + "created": "2018-10-07T02:31:49Z", + "updated": "2018-10-07T02:31:49Z", + "nameservers": [ + "1.1.1.1" + ], + "verified": "2018-10-07T02:31:49Z", + "status": "ACTIVE", + "disabled": false + } + ] +}` + + _, err := w.Write([]byte(content)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + zone, err := client.GetZones(context.Background(), "sub.foo.com") require.NoError(t, err) expected := &Zone{ID: "A", Domain: "foo.com"} diff --git a/providers/dns/stackpath/internal/fixtures/get_zone_records.json b/providers/dns/stackpath/internal/fixtures/get_zone_records.json deleted file mode 100644 index 1556d08fe..000000000 --- a/providers/dns/stackpath/internal/fixtures/get_zone_records.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "records": [ - {"id":"1","name":"foo1","type":"TXT","ttl":120,"data":"txtTXTtxt"}, - {"id":"2","name":"foo2","type":"TXT","ttl":121,"data":"TXTtxtTXT"} - ] -} diff --git a/providers/dns/stackpath/internal/fixtures/get_zones.json b/providers/dns/stackpath/internal/fixtures/get_zones.json deleted file mode 100644 index 7630ef4fe..000000000 --- a/providers/dns/stackpath/internal/fixtures/get_zones.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "pageInfo": { - "totalCount": "5", - "hasPreviousPage": false, - "hasNextPage": false, - "startCursor": "1", - "endCursor": "1" - }, - "zones": [ - { - "stackId": "my_stack", - "accountId": "my_account", - "id": "A", - "domain": "foo.com", - "version": "1", - "labels": { - "property1": "val1", - "property2": "val2" - }, - "created": "2018-10-07T02:31:49Z", - "updated": "2018-10-07T02:31:49Z", - "nameservers": [ - "1.1.1.1" - ], - "verified": "2018-10-07T02:31:49Z", - "status": "ACTIVE", - "disabled": false - } - ] -} diff --git a/providers/dns/stackpath/internal/identity.go b/providers/dns/stackpath/internal/identity.go index fa3e9df07..5c6e6ab17 100644 --- a/providers/dns/stackpath/internal/identity.go +++ b/providers/dns/stackpath/internal/identity.go @@ -9,7 +9,7 @@ import ( const defaultAuthURL = "https://gateway.stackpath.com/identity/v1/oauth2/token" -func CreateOAuthClient(ctx context.Context, clientID, clientSecret string) *http.Client { +func createOAuthClient(ctx context.Context, clientID, clientSecret string) *http.Client { config := &clientcredentials.Config{ TokenURL: defaultAuthURL, ClientID: clientID, diff --git a/providers/dns/stackpath/stackpath.go b/providers/dns/stackpath/stackpath.go index 2e193b8a9..97cfd8aa3 100644 --- a/providers/dns/stackpath/stackpath.go +++ b/providers/dns/stackpath/stackpath.go @@ -8,11 +8,9 @@ import ( "fmt" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/stackpath/internal" ) @@ -29,8 +27,6 @@ const ( EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { ClientID string @@ -44,7 +40,7 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + TTL: env.GetOrDefaultInt(EnvTTL, 120), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), } @@ -87,14 +83,9 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("stackpath: stack id missing") } - return &DNSProvider{ - config: config, - client: internal.NewClient(config.StackID, - clientdebug.Wrap( - internal.CreateOAuthClient(context.Background(), config.ClientID, config.ClientSecret), - ), - ), - }, nil + client := internal.NewClient(context.Background(), config.StackID, config.ClientID, config.ClientSecret) + + return &DNSProvider{config: config, client: client}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. diff --git a/providers/dns/stackpath/stackpath.toml b/providers/dns/stackpath/stackpath.toml index b50e7035f..63182625d 100644 --- a/providers/dns/stackpath/stackpath.toml +++ b/providers/dns/stackpath/stackpath.toml @@ -8,7 +8,7 @@ Example = ''' STACKPATH_CLIENT_ID=xxxxx \ STACKPATH_CLIENT_SECRET=yyyyy \ STACKPATH_STACK_ID=zzzzz \ -lego --dns stackpath -d '*.example.com' -d example.com run +lego --email you@example.com --dns stackpath --domains my.example.org run ''' [Configuration] @@ -17,9 +17,9 @@ lego --dns stackpath -d '*.example.com' -d example.com run STACKPATH_CLIENT_SECRET = "Client secret" STACKPATH_STACK_ID = "Stack ID" [Configuration.Additional] - STACKPATH_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - STACKPATH_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - STACKPATH_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + STACKPATH_POLLING_INTERVAL = "Time between DNS propagation check" + STACKPATH_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + STACKPATH_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://developer.stackpath.com/en/api/dns/#tag/Zone" diff --git a/providers/dns/stackpath/stackpath_test.go b/providers/dns/stackpath/stackpath_test.go index a4b959222..f8b83140f 100644 --- a/providers/dns/stackpath/stackpath_test.go +++ b/providers/dns/stackpath/stackpath_test.go @@ -72,7 +72,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -138,7 +137,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -152,7 +150,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/syse/internal/client.go b/providers/dns/syse/internal/client.go deleted file mode 100644 index 8cb801469..000000000 --- a/providers/dns/syse/internal/client.go +++ /dev/null @@ -1,131 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" -) - -const defaultBaseURL = "https://www.syse.no/api" - -// Client the Syse API client. -type Client struct { - credentials map[string]string - - BaseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(credentials map[string]string) (*Client, error) { - if len(credentials) == 0 { - return nil, errors.New("credentials missing") - } - - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - credentials: credentials, - BaseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -func (c *Client) CreateRecord(ctx context.Context, zone string, record Record) (*Record, error) { - endpoint := c.BaseURL.JoinPath("dns", zone) - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) - if err != nil { - return nil, err - } - - req.SetBasicAuth(zone, c.credentials[zone]) - - result := new(Record) - - err = c.do(req, result) - if err != nil { - return nil, err - } - - return result, nil -} - -func (c *Client) DeleteRecord(ctx context.Context, zone, recordID string) error { - endpoint := c.BaseURL.JoinPath("dns", zone, recordID) - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return err - } - - req.SetBasicAuth(zone, c.credentials[zone]) - - return c.do(req, nil) -} - -func (c *Client) do(req *http.Request, result any) error { - useragent.SetHeader(req.Header) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - raw, _ := io.ReadAll(resp.Body) - - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} diff --git a/providers/dns/syse/internal/client_test.go b/providers/dns/syse/internal/client_test.go deleted file mode 100644 index 88416aa88..000000000 --- a/providers/dns/syse/internal/client_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient(map[string]string{ - "example.com": "secret", - }) - if err != nil { - return nil, err - } - - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(), - ) -} - -func TestClient_CreateRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /dns/example.com", - servermock.ResponseFromFixture("create_record.json"), - servermock.CheckRequestJSONBodyFromFixture("create_record-request.json")). - Build(t) - - record := Record{ - Type: "TXT", - Prefix: "_acme-challenge", - Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - Active: true, - TTL: 120, - } - - result, err := client.CreateRecord(t.Context(), "example.com", record) - require.NoError(t, err) - - expected := &Record{ - ID: "1234", - Type: "TXT", - Prefix: "_acme-challenge", - Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - Active: true, - TTL: 120, - } - - assert.Equal(t, expected, result) -} - -func TestClient_CreateRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /dns/example.com", - servermock.RawStringResponse(http.StatusText(http.StatusUnauthorized)). - WithStatusCode(http.StatusUnauthorized)). - Build(t) - - record := Record{ - Type: "TXT", - Prefix: "_acme-challenge", - Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - Active: true, - TTL: 120, - } - - _, err := client.CreateRecord(t.Context(), "example.com", record) - require.EqualError(t, err, "unexpected status code: [status code: 401] body: Unauthorized") -} - -func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /dns/example.com/1234", - servermock.Noop()). - Build(t) - - err := client.DeleteRecord(t.Context(), "example.com", "1234") - require.NoError(t, err) -} - -func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /dns/example.com/1234", - servermock.RawStringResponse(http.StatusText(http.StatusUnauthorized)). - WithStatusCode(http.StatusUnauthorized)). - Build(t) - - err := client.DeleteRecord(t.Context(), "example.com", "1234") - require.EqualError(t, err, "unexpected status code: [status code: 401] body: Unauthorized") -} diff --git a/providers/dns/syse/internal/fixtures/create_record-request.json b/providers/dns/syse/internal/fixtures/create_record-request.json deleted file mode 100644 index 549a0f60f..000000000 --- a/providers/dns/syse/internal/fixtures/create_record-request.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "active": true, - "ttl": 120, - "prefix": "_acme-challenge", - "type": "TXT" -} diff --git a/providers/dns/syse/internal/fixtures/create_record.json b/providers/dns/syse/internal/fixtures/create_record.json deleted file mode 100644 index b598779c6..000000000 --- a/providers/dns/syse/internal/fixtures/create_record.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "1234", - "content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - "active": true, - "ttl": 120, - "prefix": "_acme-challenge", - "type": "TXT" -} diff --git a/providers/dns/syse/internal/types.go b/providers/dns/syse/internal/types.go deleted file mode 100644 index 4b90205e1..000000000 --- a/providers/dns/syse/internal/types.go +++ /dev/null @@ -1,11 +0,0 @@ -package internal - -type Record struct { - ID string `json:"id,omitempty"` - Type string `json:"type,omitempty"` - Prefix string `json:"prefix,omitempty"` - Content string `json:"content,omitempty"` - Priority int `json:"prio,omitempty"` - TTL int `json:"ttl,omitempty"` - Active bool `json:"active,omitempty"` -} diff --git a/providers/dns/syse/syse.go b/providers/dns/syse/syse.go deleted file mode 100644 index 29633280c..000000000 --- a/providers/dns/syse/syse.go +++ /dev/null @@ -1,186 +0,0 @@ -// Package syse implements a DNS provider for solving the DNS-01 challenge using Syse. -package syse - -import ( - "context" - "errors" - "fmt" - "net/http" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/syse/internal" -) - -// Environment variables names. -const ( - envNamespace = "SYSE_" - - EnvCredentials = envNamespace + "CREDENTIALS" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - Credentials map[string]string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 1200*time.Second), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client - - recordIDs map[string]string - recordIDsMu sync.Mutex -} - -// NewDNSProvider returns a DNSProvider instance configured for Syse. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvCredentials) - if err != nil { - return nil, fmt.Errorf("syse: %w", err) - } - - config := NewDefaultConfig() - - credentials, err := env.ParsePairs(values[EnvCredentials]) - if err != nil { - return nil, fmt.Errorf("syse: credentials: %w", err) - } - - config.Credentials = credentials - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Syse. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("syse: the configuration of the DNS provider is nil") - } - - if len(config.Credentials) == 0 { - return nil, errors.New("syse: missing credentials") - } - - for domain, password := range config.Credentials { - if domain == "" { - return nil, fmt.Errorf(`syse: missing domain: "%s:%s"`, domain, password) - } - - if password == "" { - return nil, fmt.Errorf(`syse: missing password: "%s:%s"`, domain, password) - } - } - - client, err := internal.NewClient(config.Credentials) - if err != nil { - return nil, fmt.Errorf("syse: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - recordIDs: make(map[string]string), - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("syse: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("syse: %w", err) - } - - record := internal.Record{ - Type: "TXT", - Prefix: subDomain, - Content: info.Value, - TTL: d.config.TTL, - Active: true, - } - - newRecord, err := d.client.CreateRecord(context.Background(), dns01.UnFqdn(authZone), record) - if err != nil { - return fmt.Errorf("syse: create record: %w", err) - } - - d.recordIDsMu.Lock() - d.recordIDs[token] = newRecord.ID - d.recordIDsMu.Unlock() - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("syse: could not find zone for domain %q: %w", domain, err) - } - - // gets the record's unique ID from when we created it - d.recordIDsMu.Lock() - recordID, ok := d.recordIDs[token] - d.recordIDsMu.Unlock() - - if !ok { - return fmt.Errorf("syse: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) - } - - err = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), recordID) - if err != nil { - return fmt.Errorf("syse: delete record: %w", err) - } - - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/syse/syse.toml b/providers/dns/syse/syse.toml deleted file mode 100644 index b5b1fdf47..000000000 --- a/providers/dns/syse/syse.toml +++ /dev/null @@ -1,25 +0,0 @@ -Name = "Syse" -Description = '''''' -URL = "https://www.syse.no/" -Code = "syse" -Since = "v4.30.0" - -Example = ''' -SYSE_CREDENTIALS=example.com:password \ -lego --dns syse -d '*.example.com' -d example.com run - -SYSE_CREDENTIALS=example.org:password1,example.com:password2 \ -lego --dns syse -d '*.example.org' -d example.org -d '*.example.com' -d example.com -''' - -[Configuration] - [Configuration.Credentials] - SYSE_CREDENTIALS = "Comma-separated list of `zone:password` credential pairs" - [Configuration.Additional] - SYSE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" - SYSE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 1200)" - SYSE_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - SYSE_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://www.syse.no/api/dns" diff --git a/providers/dns/syse/syse_test.go b/providers/dns/syse/syse_test.go deleted file mode 100644 index a4472aa7c..000000000 --- a/providers/dns/syse/syse_test.go +++ /dev/null @@ -1,220 +0,0 @@ -package syse - -import ( - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvCredentials).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvCredentials: "example.org:123", - }, - }, - { - desc: "success multiple domains", - envVars: map[string]string{ - EnvCredentials: "example.org:123,example.com:456,example.net:789", - }, - }, - { - desc: "invalid credentials", - envVars: map[string]string{ - EnvCredentials: ",", - }, - expected: `syse: credentials: incorrect pair: `, - }, - { - desc: "missing password", - envVars: map[string]string{ - EnvCredentials: "example.org:", - }, - expected: `syse: missing password: "example.org:"`, - }, - { - desc: "missing domain", - envVars: map[string]string{ - EnvCredentials: ":123", - }, - expected: `syse: missing domain: ":123"`, - }, - { - desc: "invalid credentials, partial", - envVars: map[string]string{ - EnvCredentials: "example.org:123,example.net", - }, - expected: "syse: credentials: incorrect pair: example.net", - }, - { - desc: "missing credentials", - envVars: map[string]string{ - EnvCredentials: "", - }, - expected: "syse: some credentials information are missing: SYSE_CREDENTIALS", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - creds map[string]string - expected string - }{ - { - desc: "success", - creds: map[string]string{"example.org": "123"}, - }, - { - desc: "success multiple domains", - creds: map[string]string{ - "example.org": "123", - "example.com": "456", - "example.net": "789", - }, - }, - { - desc: "missing credentials", - expected: "syse: missing credentials", - }, - { - desc: "missing domain", - creds: map[string]string{"": "123"}, - expected: `syse: missing domain: ":123"`, - }, - { - desc: "missing password", - creds: map[string]string{"example.org": ""}, - expected: `syse: missing password: "example.org:"`, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.Credentials = test.creds - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.Credentials = map[string]string{ - "example.org": "secret", - } - config.HTTPClient = server.Client() - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - p.client.BaseURL, _ = url.Parse(server.URL) - - return p, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("/", servermock.DumpRequest()). - Route("POST /dns/example.com", - servermock.ResponseFromInternal("create_record.json"), - servermock.CheckRequestJSONBodyFromInternal("create_record-request.json")). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("DELETE /dns/example.com/1234", - servermock.Noop()). - Build(t) - - provider.recordIDs["abc"] = "1234" - - err := provider.CleanUp("example.com", "abc", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/technitium/internal/client.go b/providers/dns/technitium/internal/client.go deleted file mode 100644 index 965638b1d..000000000 --- a/providers/dns/technitium/internal/client.go +++ /dev/null @@ -1,160 +0,0 @@ -package internal - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "strings" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - querystring "github.com/google/go-querystring/query" -) - -const statusSuccess = "ok" - -// Client the Technitium API client. -type Client struct { - apiToken string - - baseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(baseURL, apiToken string) (*Client, error) { - if apiToken == "" { - return nil, errors.New("missing credentials") - } - - if baseURL == "" { - return nil, errors.New("missing server URL") - } - - apiEndpoint, err := url.Parse(baseURL) - if err != nil { - return nil, err - } - - return &Client{ - apiToken: apiToken, - baseURL: apiEndpoint, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -// AddRecord adds a resource record for an authoritative zone. -// https://github.com/TechnitiumSoftware/DnsServer/blob/master/APIDOCS.md#add-record -func (c *Client) AddRecord(ctx context.Context, record Record) (*Record, error) { - endpoint := c.baseURL.JoinPath("api", "zones", "records", "add") - - req, err := c.newFormRequest(ctx, endpoint, record) - if err != nil { - return nil, fmt.Errorf("create request: %w", err) - } - - result := &APIResponse[AddRecordResponse]{} - - err = c.do(req, result) - if err != nil { - return nil, err - } - - if result.Status != statusSuccess { - return nil, result - } - - return result.Response.AddedRecord, nil -} - -// DeleteRecord deletes a record from an authoritative zone. -// https://github.com/TechnitiumSoftware/DnsServer/blob/master/APIDOCS.md#delete-record -func (c *Client) DeleteRecord(ctx context.Context, record Record) error { - endpoint := c.baseURL.JoinPath("api", "zones", "records", "delete") - - req, err := c.newFormRequest(ctx, endpoint, record) - if err != nil { - return fmt.Errorf("create request: %w", err) - } - - result := &APIResponse[any]{} - - err = c.do(req, result) - if err != nil { - return err - } - - if result.Status != statusSuccess { - return result - } - - return nil -} - -func (c *Client) do(req *http.Request, result any) error { - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode >= http.StatusBadRequest { - return parseError(req, resp) - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func (c *Client) newFormRequest(ctx context.Context, endpoint *url.URL, payload any) (*http.Request, error) { - values := url.Values{} - - if payload != nil { - var err error - - values, err = querystring.Values(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request body: %w", err) - } - } - - values.Set("token", c.apiToken) - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), strings.NewReader(values.Encode())) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - if payload != nil { - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIResponse[any] - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return &errAPI -} diff --git a/providers/dns/technitium/internal/client_test.go b/providers/dns/technitium/internal/client_test.go deleted file mode 100644 index cd6914918..000000000 --- a/providers/dns/technitium/internal/client_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package internal - -import ( - "net/http/httptest" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient(server.URL, "secret") - if err != nil { - return nil, err - } - - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader().WithContentTypeFromURLEncoded()) -} - -func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /api/zones/records/add", - servermock.ResponseFromFixture("add-record.json"), - servermock.CheckForm().Strict(). - With("domain", "_acme-challenge.example.com"). - With("text", "txtTXTtxt"). - With("type", "TXT"). - With("token", "secret")). - Build(t) - - record := Record{ - Domain: "_acme-challenge.example.com", - Type: "TXT", - Text: "txtTXTtxt", - } - - newRecord, err := client.AddRecord(t.Context(), record) - require.NoError(t, err) - - expected := &Record{Name: "example.com", Type: "A"} - - assert.Equal(t, expected, newRecord) -} - -func TestClient_AddRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /api/zones/records/add", - servermock.ResponseFromFixture("error.json")). - Build(t) - - record := Record{ - Domain: "_acme-challenge.example.com", - Type: "TXT", - Text: "txtTXTtxt", - } - - _, err := client.AddRecord(t.Context(), record) - require.Error(t, err) - - assert.EqualError(t, err, "Status: error, ErrorMessage: error message, StackTrace: application stack trace, InnerErrorMessage: inner exception message") -} - -func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /api/zones/records/delete", - servermock.ResponseFromFixture("delete-record.json"), - servermock.CheckForm().Strict(). - With("domain", "_acme-challenge.example.com"). - With("text", "txtTXTtxt"). - With("type", "TXT"). - With("token", "secret")). - Build(t) - - record := Record{ - Domain: "_acme-challenge.example.com", - Type: "TXT", - Text: "txtTXTtxt", - } - - err := client.DeleteRecord(t.Context(), record) - require.NoError(t, err) -} - -func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /api/zones/records/delete", - servermock.ResponseFromFixture("error.json")). - Build(t) - - record := Record{ - Domain: "_acme-challenge.example.com", - Type: "TXT", - Text: "txtTXTtxt", - } - - err := client.DeleteRecord(t.Context(), record) - require.Error(t, err) - - assert.EqualError(t, err, "Status: error, ErrorMessage: error message, StackTrace: application stack trace, InnerErrorMessage: inner exception message") -} diff --git a/providers/dns/technitium/internal/fixtures/add-record.json b/providers/dns/technitium/internal/fixtures/add-record.json deleted file mode 100644 index a57f318a3..000000000 --- a/providers/dns/technitium/internal/fixtures/add-record.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "response": { - "zone": { - "name": "example.com", - "type": "Primary", - "internal": false, - "dnssecStatus": "SignedWithNSEC", - "disabled": false - }, - "addedRecord": { - "disabled": false, - "name": "example.com", - "type": "A", - "ttl": 3600, - "rData": { - "ipAddress": "3.3.3.3" - }, - "dnssecStatus": "Unknown", - "lastUsedOn": "0001-01-01T00:00:00" - } - }, - "status": "ok" -} diff --git a/providers/dns/technitium/internal/fixtures/delete-record.json b/providers/dns/technitium/internal/fixtures/delete-record.json deleted file mode 100644 index a1c51a5d0..000000000 --- a/providers/dns/technitium/internal/fixtures/delete-record.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "response": {}, - "status": "ok" -} diff --git a/providers/dns/technitium/internal/fixtures/error.json b/providers/dns/technitium/internal/fixtures/error.json deleted file mode 100644 index 6440cde84..000000000 --- a/providers/dns/technitium/internal/fixtures/error.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "status": "error", - "errorMessage": "error message", - "stackTrace": "application stack trace", - "innerErrorMessage": "inner exception message" -} diff --git a/providers/dns/technitium/internal/types.go b/providers/dns/technitium/internal/types.go deleted file mode 100644 index 29872cd3b..000000000 --- a/providers/dns/technitium/internal/types.go +++ /dev/null @@ -1,48 +0,0 @@ -package internal - -import "fmt" - -type APIResponse[T any] struct { - Status string `json:"status"` // ok/error/invalid-token - - Response T `json:"response"` - - ErrorMessage string `json:"errorMessage"` - StackTrace string `json:"stackTrace"` - InnerErrorMessage string `json:"innerErrorMessage"` -} - -func (a *APIResponse[T]) Error() string { - msg := fmt.Sprintf("Status: %s", a.Status) - - if a.ErrorMessage != "" { - msg += fmt.Sprintf(", ErrorMessage: %s", a.ErrorMessage) - } - - if a.StackTrace != "" { - msg += fmt.Sprintf(", StackTrace: %s", a.StackTrace) - } - - if a.InnerErrorMessage != "" { - msg += fmt.Sprintf(", InnerErrorMessage: %s", a.InnerErrorMessage) - } - - return msg -} - -type AddRecordResponse struct { - Zone *Zone `json:"zone"` - AddedRecord *Record `json:"addedRecord"` -} - -type Record struct { - Name string `json:"name,omitempty" url:"-"` - Domain string `json:"domain,omitempty" url:"domain"` - Type string `json:"type,omitempty" url:"type"` - Text string `json:"text,omitempty" url:"text"` -} - -type Zone struct { - Name string `json:"name"` - Type string `json:"type"` -} diff --git a/providers/dns/technitium/technitium.go b/providers/dns/technitium/technitium.go deleted file mode 100644 index fc60c09ad..000000000 --- a/providers/dns/technitium/technitium.go +++ /dev/null @@ -1,139 +0,0 @@ -// Package technitium implements a DNS provider for solving the DNS-01 challenge using Technitium. -package technitium - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/technitium/internal" -) - -// Environment variables names. -const ( - envNamespace = "TECHNITIUM_" - - EnvServerBaseURL = envNamespace + "SERVER_BASE_URL" - EnvAPIToken = envNamespace + "API_TOKEN" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - BaseURL string - APIToken string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for Technitium. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvServerBaseURL, EnvAPIToken) - if err != nil { - return nil, fmt.Errorf("technitium: %w", err) - } - - config := NewDefaultConfig() - config.BaseURL = values[EnvServerBaseURL] - config.APIToken = values[EnvAPIToken] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Technitium. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("technitium: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.BaseURL, config.APIToken) - if err != nil { - return nil, fmt.Errorf("technitium: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - record := internal.Record{ - Domain: info.EffectiveFQDN, - Type: "TXT", - Text: info.Value, - } - - _, err := d.client.AddRecord(context.Background(), record) - if err != nil { - return fmt.Errorf("technitium: add record: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - record := internal.Record{ - Domain: info.EffectiveFQDN, - Type: "TXT", - Text: info.Value, - } - - err := d.client.DeleteRecord(context.Background(), record) - if err != nil { - return fmt.Errorf("technitium: delete record: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/technitium/technitium.toml b/providers/dns/technitium/technitium.toml deleted file mode 100644 index ac1fc6466..000000000 --- a/providers/dns/technitium/technitium.toml +++ /dev/null @@ -1,33 +0,0 @@ -Name = "Technitium" -Description = '''''' -URL = "https://technitium.com/" -Code = "technitium" -Since = "v4.20.0" - -Example = ''' -TECHNITIUM_SERVER_BASE_URL="https://localhost:5380" \ -TECHNITIUM_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns technitium -d '*.example.com' -d example.com run -''' - -Additional = ''' -Technitium DNS Server supports Dynamic Updates (RFC2136) for primary zones, -so you can also use the [RFC2136 provider](https://go-acme.github.io/lego/dns/rfc2136/index.html). - -[RFC2136 provider](https://go-acme.github.io/lego/dns/rfc2136/index.html) is much better compared to the HTTP API option from security perspective. -Technitium recommends to use it in production over the HTTP API. -''' - -[Configuration] - [Configuration.Credentials] - TECHNITIUM_SERVER_BASE_URL = "Server base URL" - TECHNITIUM_API_TOKEN = "API token" - [Configuration.Additional] - TECHNITIUM_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - TECHNITIUM_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - TECHNITIUM_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - TECHNITIUM_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://github.com/TechnitiumSoftware/DnsServer/blob/0f83d23e605956b66ac76921199e241d9cc061bd/APIDOCS.md" - Article = "https://blog.technitium.com/2023/03/" diff --git a/providers/dns/technitium/technitium_test.go b/providers/dns/technitium/technitium_test.go deleted file mode 100644 index 4eee530fd..000000000 --- a/providers/dns/technitium/technitium_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package technitium - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvServerBaseURL, EnvAPIToken).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvServerBaseURL: "https://localhost:5380", - EnvAPIToken: "secret", - }, - }, - { - desc: "missing server base URL", - envVars: map[string]string{ - EnvServerBaseURL: "", - EnvAPIToken: "secret", - }, - expected: "technitium: some credentials information are missing: TECHNITIUM_SERVER_BASE_URL", - }, - { - desc: "missing token", - envVars: map[string]string{ - EnvServerBaseURL: "https://localhost:5380", - EnvAPIToken: "", - }, - expected: "technitium: some credentials information are missing: TECHNITIUM_API_TOKEN", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "technitium: some credentials information are missing: TECHNITIUM_SERVER_BASE_URL,TECHNITIUM_API_TOKEN", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - baseURL string - token string - expected string - }{ - { - desc: "success", - baseURL: "https://localhost:5380", - token: "secret", - }, - { - desc: "missing server base URL", - token: "secret", - expected: "technitium: missing server URL", - }, - { - desc: "missing token", - baseURL: "https://localhost:5380", - expected: "technitium: missing credentials", - }, - { - desc: "missing credentials", - expected: "technitium: missing credentials", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.BaseURL = test.baseURL - config.APIToken = test.token - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/tencentcloud/tencentcloud.go b/providers/dns/tencentcloud/tencentcloud.go index 00e41e93e..448ca8ea6 100644 --- a/providers/dns/tencentcloud/tencentcloud.go +++ b/providers/dns/tencentcloud/tencentcloud.go @@ -2,18 +2,16 @@ package tencentcloud import ( - "context" "errors" "fmt" "math" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - dnspod "github.com/go-acme/tencentclouddnspod/v20210323" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" + dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323" ) // Environment variables names. @@ -31,8 +29,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { SecretID string @@ -118,9 +114,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) - ctx := context.Background() - - zone, err := d.getHostedZone(ctx, info.EffectiveFQDN) + zone, err := d.getHostedZone(info.EffectiveFQDN) if err != nil { return fmt.Errorf("tencentcloud: failed to get hosted zone: %w", err) } @@ -139,7 +133,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { request.Value = common.StringPtr(info.Value) request.TTL = common.Uint64Ptr(uint64(d.config.TTL)) - _, err = dnspod.CreateRecordWithContext(ctx, d.client, request) + _, err = d.client.CreateRecord(request) if err != nil { return fmt.Errorf("dnspod: API call failed: %w", err) } @@ -151,14 +145,12 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) - ctx := context.Background() - - zone, err := d.getHostedZone(ctx, info.EffectiveFQDN) + zone, err := d.getHostedZone(info.EffectiveFQDN) if err != nil { return fmt.Errorf("tencentcloud: failed to get hosted zone: %w", err) } - records, err := d.findTxtRecords(ctx, zone, info.EffectiveFQDN) + records, err := d.findTxtRecords(zone, info.EffectiveFQDN) if err != nil { return fmt.Errorf("tencentcloud: failed to find TXT records: %w", err) } @@ -169,7 +161,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { request.DomainId = zone.DomainId request.RecordId = record.RecordId - _, err := dnspod.DeleteRecordWithContext(ctx, d.client, request) + _, err := d.client.DeleteRecord(request) if err != nil { return fmt.Errorf("tencentcloud: delete record failed: %w", err) } diff --git a/providers/dns/tencentcloud/tencentcloud.toml b/providers/dns/tencentcloud/tencentcloud.toml index 50f4ee9d5..4338e1daf 100644 --- a/providers/dns/tencentcloud/tencentcloud.toml +++ b/providers/dns/tencentcloud/tencentcloud.toml @@ -1,13 +1,13 @@ Name = "Tencent Cloud DNS" Description = '''''' -URL = "https://cloud.tencent.com/product/dns" +URL = "https://cloud.tencent.com/product/cns" Code = "tencentcloud" Since = "v4.6.0" Example = ''' TENCENTCLOUD_SECRET_ID=abcdefghijklmnopqrstuvwx \ TENCENTCLOUD_SECRET_KEY=your-secret-key \ -lego --dns tencentcloud -d '*.example.com' -d example.com run +lego --email you@example.com --dns tencentcloud --domains my.example.org run ''' [Configuration] @@ -17,10 +17,10 @@ lego --dns tencentcloud -d '*.example.com' -d example.com run [Configuration.Additional] TENCENTCLOUD_SESSION_TOKEN = "Access Key token" TENCENTCLOUD_REGION = "Region" - TENCENTCLOUD_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - TENCENTCLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - TENCENTCLOUD_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)" - TENCENTCLOUD_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + TENCENTCLOUD_POLLING_INTERVAL = "Time between DNS propagation check" + TENCENTCLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + TENCENTCLOUD_TTL = "The TTL of the TXT record used for the DNS challenge" + TENCENTCLOUD_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://cloud.tencent.com/document/product/1427/56153" diff --git a/providers/dns/tencentcloud/tencentcloud_test.go b/providers/dns/tencentcloud/tencentcloud_test.go index ce6358174..c5a2fd974 100644 --- a/providers/dns/tencentcloud/tencentcloud_test.go +++ b/providers/dns/tencentcloud/tencentcloud_test.go @@ -55,7 +55,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -128,7 +127,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -142,7 +140,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/tencentcloud/wrapper.go b/providers/dns/tencentcloud/wrapper.go index 6a66bc1c6..32b66d523 100644 --- a/providers/dns/tencentcloud/wrapper.go +++ b/providers/dns/tencentcloud/wrapper.go @@ -1,24 +1,23 @@ package tencentcloud import ( - "context" "errors" "fmt" "github.com/go-acme/lego/v4/challenge/dns01" - dnspod "github.com/go-acme/tencentclouddnspod/v20210323" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" errorsdk "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors" + dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323" "golang.org/x/net/idna" ) -func (d *DNSProvider) getHostedZone(ctx context.Context, domain string) (*dnspod.DomainListItem, error) { +func (d *DNSProvider) getHostedZone(domain string) (*dnspod.DomainListItem, error) { request := dnspod.NewDescribeDomainListRequest() var domains []*dnspod.DomainListItem for { - response, err := dnspod.DescribeDomainListWithContext(ctx, d.client, request) + response, err := d.client.DescribeDomainList(request) if err != nil { return nil, fmt.Errorf("API call failed: %w", err) } @@ -38,7 +37,6 @@ func (d *DNSProvider) getHostedZone(ctx context.Context, domain string) (*dnspod } var hostedZone *dnspod.DomainListItem - for _, zone := range domains { unfqdn := dns01.UnFqdn(authZone) if *zone.Name == unfqdn || *zone.Punycode == unfqdn { @@ -53,7 +51,7 @@ func (d *DNSProvider) getHostedZone(ctx context.Context, domain string) (*dnspod return hostedZone, nil } -func (d *DNSProvider) findTxtRecords(ctx context.Context, zone *dnspod.DomainListItem, fqdn string) ([]*dnspod.RecordListItem, error) { +func (d *DNSProvider) findTxtRecords(zone *dnspod.DomainListItem, fqdn string) ([]*dnspod.RecordListItem, error) { recordName, err := extractRecordName(fqdn, *zone.Name) if err != nil { return nil, err @@ -66,7 +64,7 @@ func (d *DNSProvider) findTxtRecords(ctx context.Context, zone *dnspod.DomainLis request.RecordType = common.StringPtr("TXT") request.RecordLine = common.StringPtr("默认") - response, err := dnspod.DescribeRecordListWithContext(ctx, d.client, request) + response, err := d.client.DescribeRecordList(request) if err != nil { var sdkError *errorsdk.TencentCloudSDKError if errors.As(err, &sdkError) { @@ -74,7 +72,6 @@ func (d *DNSProvider) findTxtRecords(ctx context.Context, zone *dnspod.DomainLis return nil, nil } } - return nil, err } diff --git a/providers/dns/timewebcloud/internal/client.go b/providers/dns/timewebcloud/internal/client.go deleted file mode 100644 index ec3c8703d..000000000 --- a/providers/dns/timewebcloud/internal/client.go +++ /dev/null @@ -1,151 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "golang.org/x/oauth2" -) - -const defaultBaseURL = "https://api.timeweb.cloud/api" - -// Client Timeweb Cloud client. -type Client struct { - baseURL *url.URL - httpClient *http.Client -} - -// NewClient creates a Client. -func NewClient(hc *http.Client) *Client { - baseURL, _ := url.Parse(defaultBaseURL) - - if hc == nil { - hc = &http.Client{Timeout: 10 * time.Second} - } - - return &Client{ - baseURL: baseURL, - httpClient: hc, - } -} - -// CreateRecord creates a DNS record. -// https://timeweb.cloud/api-docs#tag/Domeny/operation/createDomainDNSRecord -func (c *Client) CreateRecord(ctx context.Context, zone string, record DNSRecord) (*DNSRecord, error) { - endpoint := c.baseURL.JoinPath("v1", "domains", dns01.UnFqdn(zone), "dns-records") - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) - if err != nil { - return nil, err - } - - respData := &CreateRecordResponse{} - - err = c.do(req, respData) - if err != nil { - return nil, err - } - - return respData.DNSRecord, nil -} - -// DeleteRecord deletes a DNS record. -// https://timeweb.cloud/api-docs#tag/Domeny/operation/deleteDomainDNSRecord -func (c *Client) DeleteRecord(ctx context.Context, zone string, recordID int) error { - endpoint := c.baseURL.JoinPath("v1", "domains", dns01.UnFqdn(zone), "dns-records", strconv.Itoa(recordID)) - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return err - } - - return c.do(req, nil) -} - -func (c *Client) do(req *http.Request, result any) error { - resp, err := c.httpClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var response ErrorResponse - - err := json.Unmarshal(raw, &response) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return response -} - -func OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client { - if client == nil { - client = &http.Client{Timeout: 10 * time.Second} - } - - client.Transport = &oauth2.Transport{ - Source: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}), - Base: client.Transport, - } - - return client -} diff --git a/providers/dns/timewebcloud/internal/client_test.go b/providers/dns/timewebcloud/internal/client_test.go deleted file mode 100644 index 9d16ba4c5..000000000 --- a/providers/dns/timewebcloud/internal/client_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient(OAuthStaticAccessToken(server.Client(), "secret")) - client.baseURL, _ = url.Parse(server.URL) - - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("Bearer secret"), - ) -} - -func TestClient_CreateRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /v1/domains/example.com/dns-records", - servermock.ResponseFromFixture("createDomainDNSRecord.json"), - servermock.CheckRequestJSONBody(`{"type":"TXT","value":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","subdomain":"_acme-challenge"}`)). - Build(t) - - payload := DNSRecord{ - Type: "TXT", - Value: "w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI", - SubDomain: "_acme-challenge", - } - - response, err := client.CreateRecord(t.Context(), "example.com.", payload) - require.NoError(t, err) - - expected := &DNSRecord{ - Type: "TXT", - ID: 123, - } - - assert.Equal(t, expected, response) -} - -func TestClient_CreateRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /v1/domains/example.com/dns-records", - servermock.ResponseFromFixture("error_bad_request.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) - - _, err := client.CreateRecord(t.Context(), "example.com.", DNSRecord{}) - require.Error(t, err) - - assert.EqualError(t, err, "400: Value must be a number conforming to the specified constraints (bad_request) [15095f25-aac3-4d60-a788-96cb5136f186]") -} - -func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /v1/domains/example.com/dns-records/123", - servermock.Noop(). - WithStatusCode(http.StatusNoContent)). - Build(t) - - err := client.DeleteRecord(t.Context(), "example.com.", 123) - require.NoError(t, err) -} - -func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /v1/domains/example.com/dns-records/123", - servermock.ResponseFromFixture("error_unauthorized.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) - - err := client.DeleteRecord(t.Context(), "example.com.", 123) - require.Error(t, err) - - assert.EqualError(t, err, "401: Unauthorized (unauthorized) [15095f25-aac3-4d60-a788-96cb5136f186]") -} diff --git a/providers/dns/timewebcloud/internal/fixtures/createDomainDNSRecord.json b/providers/dns/timewebcloud/internal/fixtures/createDomainDNSRecord.json deleted file mode 100644 index 361c2b5d5..000000000 --- a/providers/dns/timewebcloud/internal/fixtures/createDomainDNSRecord.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "dns_record": { - "type": "TXT", - "id": 123, - "data": { - "priority": 0, - "subdomain": "example.com", - "value": "w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI" - } - }, - "response_id": "15095f25-aac3-4d60-a788-96cb5136f186" -} diff --git a/providers/dns/timewebcloud/internal/fixtures/error_bad_request.json b/providers/dns/timewebcloud/internal/fixtures/error_bad_request.json deleted file mode 100644 index 34d83c164..000000000 --- a/providers/dns/timewebcloud/internal/fixtures/error_bad_request.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - - "status_code": 400, - "message": "Value must be a number conforming to the specified constraints", - "error_code": "bad_request", - "response_id": "15095f25-aac3-4d60-a788-96cb5136f186" - -} diff --git a/providers/dns/timewebcloud/internal/fixtures/error_unauthorized.json b/providers/dns/timewebcloud/internal/fixtures/error_unauthorized.json deleted file mode 100644 index b8af2b61b..000000000 --- a/providers/dns/timewebcloud/internal/fixtures/error_unauthorized.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "status_code": 401, - "message": "Unauthorized", - "error_code": "unauthorized", - "response_id": "15095f25-aac3-4d60-a788-96cb5136f186" -} diff --git a/providers/dns/timewebcloud/internal/readme.md b/providers/dns/timewebcloud/internal/readme.md deleted file mode 100644 index b6db50d4f..000000000 --- a/providers/dns/timewebcloud/internal/readme.md +++ /dev/null @@ -1,8 +0,0 @@ -There is an [official API client](https://github.com/timeweb-cloud/sdk-go) but this client is completely broken: -- the code is generated and the module name is `github.com/GIT_USER_ID/GIT_REPO_ID` -- the code contains redeclared constants -- Even with fixes to the module name and the redeclared constants, the module doesn't compile. - -https://github.com/timeweb-cloud/sdk-go/pull/1 - -So, for now, this API client is unusable. diff --git a/providers/dns/timewebcloud/internal/types.go b/providers/dns/timewebcloud/internal/types.go deleted file mode 100644 index 80cdb2c70..000000000 --- a/providers/dns/timewebcloud/internal/types.go +++ /dev/null @@ -1,27 +0,0 @@ -package internal - -import "fmt" - -type DNSRecord struct { - ID int `json:"id,omitempty"` - Type string `json:"type,omitempty"` - Value string `json:"value,omitempty"` - - // SubDomain is the full name of a subdomain (not only the subdomain label). - SubDomain string `json:"subdomain,omitempty"` -} - -type CreateRecordResponse struct { - DNSRecord *DNSRecord `json:"dns_record,omitempty"` -} - -type ErrorResponse struct { - StatusCode int `json:"status_code,omitempty"` - ErrorCode string `json:"error_code,omitempty"` - Message string `json:"message,omitempty"` - ResponseID string `json:"response_id,omitempty"` -} - -func (a ErrorResponse) Error() string { - return fmt.Sprintf("%d: %s (%s) [%s]", a.StatusCode, a.Message, a.ErrorCode, a.ResponseID) -} diff --git a/providers/dns/timewebcloud/timewebcloud.go b/providers/dns/timewebcloud/timewebcloud.go deleted file mode 100644 index a599566e3..000000000 --- a/providers/dns/timewebcloud/timewebcloud.go +++ /dev/null @@ -1,158 +0,0 @@ -// Package timewebcloud implements a DNS provider for solving the DNS-01 challenge using Timeweb Cloud. -package timewebcloud - -import ( - "context" - "errors" - "fmt" - "net/http" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/timewebcloud/internal" -) - -// Environment variables names. -const ( - envNamespace = "TIMEWEBCLOUD_" - - EnvAuthToken = envNamespace + "AUTH_TOKEN" - - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - AuthToken string - - HTTPClient *http.Client - PropagationTimeout time.Duration - PollingInterval time.Duration -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client - - recordIDs map[string]int - recordIDsMu sync.Mutex -} - -// NewDNSProvider returns a DNSProvider instance configured for Timeweb Cloud. -// API token must be passed in the environment variable TIMEWEBCLOUD_TOKEN. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAuthToken) - if err != nil { - return nil, fmt.Errorf("timewebcloud: %w", err) - } - - config := NewDefaultConfig() - config.AuthToken = values[EnvAuthToken] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig returns a DNSProvider instance configured for Timeweb Cloud. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("timewebcloud: the configuration of the DNS provider is nil") - } - - if config.AuthToken == "" { - return nil, errors.New("timewebcloud: authentication token is missing") - } - - client := internal.NewClient( - clientdebug.Wrap( - internal.OAuthStaticAccessToken(config.HTTPClient, config.AuthToken), - ), - ) - - return &DNSProvider{ - config: config, - client: client, - recordIDs: make(map[string]int), - }, nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -// Present creates a TXT record to fulfill the dns-01 challenge. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("timewebcloud: could not find zone for domain %q: %w", domain, err) - } - - record := internal.DNSRecord{ - Type: "TXT", - Value: info.Value, - SubDomain: dns01.UnFqdn(info.EffectiveFQDN), - } - - response, err := d.client.CreateRecord(context.Background(), authZone, record) - if err != nil { - return fmt.Errorf("timewebcloud: %w", err) - } - - d.recordIDsMu.Lock() - d.recordIDs[token] = response.ID - d.recordIDsMu.Unlock() - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("timewebcloud: could not find zone for domain %q: %w", domain, err) - } - - d.recordIDsMu.Lock() - recordID, ok := d.recordIDs[token] - d.recordIDsMu.Unlock() - - if !ok { - return fmt.Errorf("timewebcloud: unknown record ID for '%s'", info.EffectiveFQDN) - } - - err = d.client.DeleteRecord(context.Background(), authZone, recordID) - if err != nil { - return fmt.Errorf("timewebcloud: %w", err) - } - - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - - return nil -} diff --git a/providers/dns/timewebcloud/timewebcloud.toml b/providers/dns/timewebcloud/timewebcloud.toml deleted file mode 100644 index c8bde636a..000000000 --- a/providers/dns/timewebcloud/timewebcloud.toml +++ /dev/null @@ -1,21 +0,0 @@ -Name = "Timeweb Cloud" -Description = '''''' -URL = "https://timeweb.cloud/" -Code = "timewebcloud" -Since = "v4.20.0" - -Example = ''' -TIMEWEBCLOUD_AUTH_TOKEN=xxxxxx \ -lego --dns timewebcloud -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - TIMEWEBCLOUD_AUTH_TOKEN = "Authentication token" - [Configuration.Additional] - TIMEWEBCLOUD_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - TIMEWEBCLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - TIMEWEBCLOUD_HTTP_TIMEOUT = "API request timeout in seconds (Default: 10)" - -[Links] - API = "https://timeweb.cloud/api-docs" diff --git a/providers/dns/timewebcloud/timewebcloud_test.go b/providers/dns/timewebcloud/timewebcloud_test.go deleted file mode 100644 index 26e107578..000000000 --- a/providers/dns/timewebcloud/timewebcloud_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package timewebcloud - -import ( - "testing" - "time" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAuthToken).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAuthToken: "johndoe", - }, - }, - { - desc: "missing credentials", - envVars: map[string]string{ - EnvAuthToken: "", - }, - expected: "timewebcloud: some credentials information are missing: TIMEWEBCLOUD_AUTH_TOKEN", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - require.NotNil(t, p.recordIDs) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - authToken string - expected string - }{ - { - desc: "success", - authToken: "123", - }, - { - desc: "missing credentials", - expected: "timewebcloud: authentication token is missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.AuthToken = test.authToken - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - require.NotNil(t, p.recordIDs) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - time.Sleep(1 * time.Second) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/todaynic/internal/client.go b/providers/dns/todaynic/internal/client.go deleted file mode 100644 index 2c537f4a7..000000000 --- a/providers/dns/todaynic/internal/client.go +++ /dev/null @@ -1,141 +0,0 @@ -package internal - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" - querystring "github.com/google/go-querystring/query" -) - -const defaultBaseURL = "https://todapi.now.cn:2443" - -// Client the TodayNIC API client. -type Client struct { - authUserID string - apiKey string - - BaseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(authUserID, apiKey string) (*Client, error) { - if authUserID == "" || apiKey == "" { - return nil, errors.New("credentials missing") - } - - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - authUserID: authUserID, - apiKey: apiKey, - BaseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -func (c *Client) AddRecord(ctx context.Context, record Record) (int, error) { - endpoint := c.BaseURL.JoinPath("api", "dns", "add-domain-record.json") - - query, err := querystring.Values(record) - if err != nil { - return 0, err - } - - req, err := c.newRequest(ctx, endpoint, query) - if err != nil { - return 0, err - } - - var result APIResponse - - err = c.do(req, &result) - if err != nil { - return 0, err - } - - return result.ID, nil -} - -func (c *Client) DeleteRecord(ctx context.Context, recordID int) error { - endpoint := c.BaseURL.JoinPath("api", "dns", "delete-domain-record.json") - - query := endpoint.Query() - query.Set("Id", strconv.Itoa(recordID)) - - req, err := c.newRequest(ctx, endpoint, query) - if err != nil { - return err - } - - return c.do(req, nil) -} - -func (c *Client) do(req *http.Request, result any) error { - useragent.SetHeader(req.Header) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func (c *Client) newRequest(ctx context.Context, endpoint *url.URL, query url.Values) (*http.Request, error) { - query.Set("auth-userid", c.authUserID) - query.Set("api-key", c.apiKey) - - endpoint.RawQuery = query.Encode() - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIError - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return &errAPI -} diff --git a/providers/dns/todaynic/internal/client_test.go b/providers/dns/todaynic/internal/client_test.go deleted file mode 100644 index 71ee7f8b7..000000000 --- a/providers/dns/todaynic/internal/client_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("user123", "secret") - if err != nil { - return nil, err - } - - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(), - ) -} - -func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("GET /api/dns/add-domain-record.json", - servermock.ResponseFromFixture("add_record.json"), - servermock.CheckQueryParameter().Strict(). - With("Domain", "example.com"). - With("Host", "_acme-challenge"). - With("Type", "TXT"). - With("Value", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"). - With("Ttl", "600"). - With("auth-userid", "user123"). - With("api-key", "secret"), - ). - Build(t) - - record := Record{ - Domain: "example.com", - Host: "_acme-challenge", - Type: "TXT", - Value: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: "600", - } - - recordID, err := client.AddRecord(t.Context(), record) - require.NoError(t, err) - - assert.Equal(t, 11554102, recordID) -} - -func TestClient_AddRecord_error(t *testing.T) { - client := mockBuilder(). - Route("GET /api/dns/add-domain-record.json", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusNotFound), - ). - Build(t) - - record := Record{ - Domain: "example.com", - Host: "_acme-challenge", - Type: "TXT", - Value: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY", - TTL: "600", - } - - _, err := client.AddRecord(t.Context(), record) - require.EqualError(t, err, "host.repeat (2d5876b2-f272-43e9-acc1-4c6a3d3683b1)") -} - -func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("GET /api/dns/delete-domain-record.json", - servermock.ResponseFromFixture("add_record.json"), - servermock.CheckQueryParameter().Strict(). - With("Id", "123"). - With("auth-userid", "user123"). - With("api-key", "secret"), - ). - Build(t) - - err := client.DeleteRecord(t.Context(), 123) - require.NoError(t, err) -} diff --git a/providers/dns/todaynic/internal/fixtures/add_record.json b/providers/dns/todaynic/internal/fixtures/add_record.json deleted file mode 100644 index 27f34d71c..000000000 --- a/providers/dns/todaynic/internal/fixtures/add_record.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "RequestId": "f60ea4d9-67ef-49fa-bbae-06178a6e7293", - "Id": 11554102 -} diff --git a/providers/dns/todaynic/internal/fixtures/error.json b/providers/dns/todaynic/internal/fixtures/error.json deleted file mode 100644 index 3ea9c9310..000000000 --- a/providers/dns/todaynic/internal/fixtures/error.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "RequestId": "2d5876b2-f272-43e9-acc1-4c6a3d3683b1", - "error": "host.repeat" -} diff --git a/providers/dns/todaynic/internal/types.go b/providers/dns/todaynic/internal/types.go deleted file mode 100644 index 0a15c7da8..000000000 --- a/providers/dns/todaynic/internal/types.go +++ /dev/null @@ -1,26 +0,0 @@ -package internal - -import "fmt" - -type APIError struct { - RequestID string `json:"RequestId"` - Message string `json:"error"` -} - -func (a *APIError) Error() string { - return fmt.Sprintf("%s (%s)", a.Message, a.RequestID) -} - -type Record struct { - Domain string `url:"Domain,omitempty"` - Host string `url:"Host,omitempty"` - Type string `url:"Type,omitempty"` - Value string `url:"Value,omitempty"` - Mx string `url:"Mx,omitempty"` - TTL string `url:"Ttl,omitempty"` -} - -type APIResponse struct { - RequestID string `json:"RequestId"` - ID int `json:"Id"` -} diff --git a/providers/dns/todaynic/todaynic.go b/providers/dns/todaynic/todaynic.go deleted file mode 100644 index 3a3734033..000000000 --- a/providers/dns/todaynic/todaynic.go +++ /dev/null @@ -1,164 +0,0 @@ -// Package todaynic implements a DNS provider for solving the DNS-01 challenge using TodayNIC. -package todaynic - -import ( - "context" - "errors" - "fmt" - "net/http" - "strconv" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/todaynic/internal" -) - -// Environment variables names. -const ( - envNamespace = "TODAYNIC_" - - EnvAuthUserID = envNamespace + "AUTH_USER_ID" - EnvAPIKey = envNamespace + "API_KEY" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - AuthUserID string - APIKey string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, 600), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client - - recordIDs map[string]int - recordIDsMu sync.Mutex -} - -// NewDNSProvider returns a DNSProvider instance configured for TodayNIC. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAuthUserID, EnvAPIKey) - if err != nil { - return nil, fmt.Errorf("todaynic: %w", err) - } - - config := NewDefaultConfig() - config.AuthUserID = values[EnvAuthUserID] - config.APIKey = values[EnvAPIKey] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for TodayNIC. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("todaynic: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.AuthUserID, config.APIKey) - if err != nil { - return nil, fmt.Errorf("todaynic: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - recordIDs: make(map[string]int), - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("todaynic: could not find zone for domain %q: %w", domain, err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) - if err != nil { - return fmt.Errorf("todaynic: %w", err) - } - - record := internal.Record{ - Domain: dns01.UnFqdn(authZone), - Host: subDomain, - Type: "TXT", - Value: info.Value, - TTL: strconv.Itoa(d.config.TTL), - } - - recordID, err := d.client.AddRecord(context.Background(), record) - if err != nil { - return fmt.Errorf("todaynic: add record: %w", err) - } - - d.recordIDsMu.Lock() - d.recordIDs[token] = recordID - d.recordIDsMu.Unlock() - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - d.recordIDsMu.Lock() - recordID, ok := d.recordIDs[token] - d.recordIDsMu.Unlock() - - if !ok { - return fmt.Errorf("todaynic: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) - } - - err := d.client.DeleteRecord(context.Background(), recordID) - if err != nil { - return fmt.Errorf("todaynic: delete record: %w", err) - } - - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/todaynic/todaynic.toml b/providers/dns/todaynic/todaynic.toml deleted file mode 100644 index 16d55ccc0..000000000 --- a/providers/dns/todaynic/todaynic.toml +++ /dev/null @@ -1,25 +0,0 @@ -Name = "TodayNIC/时代互联" -Description = '''''' -URL = "https://www.todaynic.com/" -Code = "todaynic" -Since = "v4.32.0" - -Example = ''' -TODAYNIC_AUTH_USER_ID="xxx" \ -TODAYNIC_API_KEY="yyy" \ -lego --dns todaynic -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - TODAYNIC_AUTH_USER_ID = "account ID" - TODAYNIC_API_KEY = "API key" - [Configuration.Additional] - TODAYNIC_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - TODAYNIC_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - TODAYNIC_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)" - TODAYNIC_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://www.todaynic.com/partner/mode_Http_Api_detail.php" - apipost = "https://docs.apipost.net/docs/detail/49dcef10a876000?target_id=0" diff --git a/providers/dns/todaynic/todaynic_test.go b/providers/dns/todaynic/todaynic_test.go deleted file mode 100644 index c73bf6cc5..000000000 --- a/providers/dns/todaynic/todaynic_test.go +++ /dev/null @@ -1,207 +0,0 @@ -package todaynic - -import ( - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAuthUserID, EnvAPIKey).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAuthUserID: "user123", - EnvAPIKey: "secret", - }, - }, - { - desc: "missing user ID", - envVars: map[string]string{ - EnvAuthUserID: "", - EnvAPIKey: "secret", - }, - expected: "todaynic: some credentials information are missing: TODAYNIC_AUTH_USER_ID", - }, - { - desc: "missing API key", - envVars: map[string]string{ - EnvAuthUserID: "user123", - EnvAPIKey: "", - }, - expected: "todaynic: some credentials information are missing: TODAYNIC_API_KEY", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "todaynic: some credentials information are missing: TODAYNIC_AUTH_USER_ID,TODAYNIC_API_KEY", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - authUserID string - apiKey string - expected string - }{ - { - desc: "success", - authUserID: "user123", - apiKey: "secret", - }, - { - desc: "missing user ID", - apiKey: "secret", - expected: "todaynic: credentials missing", - }, - { - desc: "missing API key", - authUserID: "user123", - expected: "todaynic: credentials missing", - }, - { - desc: "missing credentials", - expected: "todaynic: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.AuthUserID = test.authUserID - config.APIKey = test.apiKey - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.AuthUserID = "user123" - config.APIKey = "secret" - config.HTTPClient = server.Client() - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - p.client.BaseURL, _ = url.Parse(server.URL) - - return p, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("GET /api/dns/add-domain-record.json", - servermock.ResponseFromInternal("add_record.json"), - servermock.CheckQueryParameter().Strict(). - With("Domain", "example.com"). - With("Host", "_acme-challenge"). - With("Type", "TXT"). - With("Value", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"). - With("Ttl", "600"). - With("auth-userid", "user123"). - With("api-key", "secret"), - ). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("GET /api/dns/delete-domain-record.json", - servermock.ResponseFromInternal("add_record.json"), - servermock.CheckQueryParameter().Strict(). - With("Id", "123"). - With("auth-userid", "user123"). - With("api-key", "secret"), - ). - Build(t) - - provider.recordIDs["abc"] = 123 - - err := provider.CleanUp("example.com", "abc", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/transip/fakeclient_test.go b/providers/dns/transip/fakeclient_test.go new file mode 100644 index 000000000..b1ee0434e --- /dev/null +++ b/providers/dns/transip/fakeclient_test.go @@ -0,0 +1,136 @@ +package transip + +import ( + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/transip/gotransip/v6/domain" + "github.com/transip/gotransip/v6/rest" +) + +type dnsEntryWrapper struct { + DNSEntry domain.DNSEntry `json:"dnsEntry"` +} + +type dnsEntriesWrapper struct { + DNSEntries []domain.DNSEntry `json:"dnsEntries"` +} + +type fakeClient struct { + dnsEntries []domain.DNSEntry + setDNSEntriesLatency time.Duration + getInfoLatency time.Duration + domainName string +} + +func (f *fakeClient) PutWithResponse(_ rest.Request) (rest.Response, error) { + panic("not implemented") +} + +func (f *fakeClient) PostWithResponse(_ rest.Request) (rest.Response, error) { + panic("not implemented") +} + +func (f *fakeClient) PatchWithResponse(_ rest.Request) (rest.Response, error) { + panic("not implemented") +} + +func (f *fakeClient) Get(request rest.Request, dest interface{}) error { + if f.getInfoLatency != 0 { + time.Sleep(f.getInfoLatency) + } + + if request.Endpoint != fmt.Sprintf("/domains/%s/dns", f.domainName) { + return fmt.Errorf("function GET for endpoint %s not implemented", request.Endpoint) + } + + entries := dnsEntriesWrapper{DNSEntries: f.dnsEntries} + + body, err := json.Marshal(entries) + if err != nil { + return fmt.Errorf("can't encode json: %w", err) + } + + err = json.Unmarshal(body, dest) + if err != nil { + return fmt.Errorf("can't decode json: %w", err) + } + + return nil +} + +func (f *fakeClient) Put(request rest.Request) error { + if f.getInfoLatency != 0 { + time.Sleep(f.getInfoLatency) + } + + return fmt.Errorf("function PUT for endpoint %s not implemented", request.Endpoint) +} + +func (f *fakeClient) Post(request rest.Request) error { + if f.getInfoLatency != 0 { + time.Sleep(f.getInfoLatency) + } + + if request.Endpoint != fmt.Sprintf("/domains/%s/dns", f.domainName) { + return fmt.Errorf("function POST for endpoint %s not implemented", request.Endpoint) + } + + body, err := request.GetJSONBody() + if err != nil { + return errors.New("unable get request body") + } + + var entry dnsEntryWrapper + if err := json.Unmarshal(body, &entry); err != nil { + return errors.New("unable to decode request body") + } + + f.dnsEntries = append(f.dnsEntries, entry.DNSEntry) + + return nil +} + +func (f *fakeClient) Delete(request rest.Request) error { + if f.getInfoLatency != 0 { + time.Sleep(f.getInfoLatency) + } + + if request.Endpoint != fmt.Sprintf("/domains/%s/dns", f.domainName) { + return fmt.Errorf("function DELETE for endpoint %s not implemented", request.Endpoint) + } + + body, err := request.GetJSONBody() + if err != nil { + return errors.New("unable get request body") + } + + var entry dnsEntryWrapper + if err := json.Unmarshal(body, &entry); err != nil { + return errors.New("unable to decode request body") + } + + cp := make([]domain.DNSEntry, 0) + + for _, e := range f.dnsEntries { + if e.Name == entry.DNSEntry.Name { + continue + } + + cp = append(cp, e) + } + + f.dnsEntries = cp + + return nil +} + +func (f *fakeClient) Patch(request rest.Request) error { + if f.getInfoLatency != 0 { + time.Sleep(f.getInfoLatency) + } + + return fmt.Errorf("function PATCH for endpoint %s not implemented", request.Endpoint) +} diff --git a/providers/dns/transip/transip.go b/providers/dns/transip/transip.go index bc2913aa4..a3b18d862 100644 --- a/providers/dns/transip/transip.go +++ b/providers/dns/transip/transip.go @@ -4,10 +4,8 @@ package transip import ( "errors" "fmt" - "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/transip/gotransip/v6" @@ -24,11 +22,8 @@ const ( EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { AccountName string @@ -36,7 +31,6 @@ type Config struct { PropagationTimeout time.Duration PollingInterval time.Duration TTL int64 - HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. @@ -45,9 +39,6 @@ func NewDefaultConfig() *Config { TTL: int64(env.GetOrDefaultInt(EnvTTL, 10)), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, } } @@ -79,19 +70,10 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("transip: the configuration of the DNS provider is nil") } - cfg := gotransip.ClientConfiguration{ + client, err := gotransip.NewClient(gotransip.ClientConfiguration{ AccountName: config.AccountName, PrivateKeyPath: config.PrivateKeyPath, - } - - if config.HTTPClient != nil { - cfg.HTTPClient = config.HTTPClient - } else { - // Uses an explicit default HTTP client because the desec.NewDefaultClientOptions uses the http.DefaultClient. - cfg.HTTPClient = &http.Client{Timeout: 30 * time.Second} - } - - client, err := gotransip.NewClient(cfg) + }) if err != nil { return nil, fmt.Errorf("transip: %w", err) } @@ -168,7 +150,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { if err = d.repository.RemoveDNSEntry(domainName, entry); err != nil { return fmt.Errorf("transip: couldn't get Record ID in CleanUp: %w", err) } - return nil } } diff --git a/providers/dns/transip/transip.toml b/providers/dns/transip/transip.toml index bf7d58ee3..c4733f431 100644 --- a/providers/dns/transip/transip.toml +++ b/providers/dns/transip/transip.toml @@ -7,7 +7,7 @@ Since = "v2.0.0" Example = ''' TRANSIP_ACCOUNT_NAME = "Account name" \ TRANSIP_PRIVATE_KEY_PATH = "transip.key" \ -lego --dns transip -d '*.example.com' -d example.com run +lego --email you@example.com --dns transip --domains my.example.org run ''' [Configuration] @@ -15,10 +15,9 @@ lego --dns transip -d '*.example.com' -d example.com run TRANSIP_ACCOUNT_NAME = "Account name" TRANSIP_PRIVATE_KEY_PATH = "Private key path" [Configuration.Additional] - TRANSIP_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" - TRANSIP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 600)" - TRANSIP_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 10)" - TRANSIP_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + TRANSIP_POLLING_INTERVAL = "Time between DNS propagation check" + TRANSIP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + TRANSIP_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://api.transip.eu/rest/docs.html" diff --git a/providers/dns/transip/transip_test.go b/providers/dns/transip/transip_test.go index 3c6e86657..05d701584 100644 --- a/providers/dns/transip/transip_test.go +++ b/providers/dns/transip/transip_test.go @@ -1,12 +1,17 @@ package transip import ( + "fmt" "os" + "strings" + "sync" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/transip/gotransip/v6/domain" ) const envDomain = envNamespace + "DOMAIN" @@ -58,7 +63,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -80,7 +84,6 @@ func TestNewDNSProvider(t *testing.T) { // Therefore, we test if the error type is the same. t.Run("could not open private key path", func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(map[string]string{ @@ -152,13 +155,113 @@ func TestNewDNSProviderConfig(t *testing.T) { }) } +func TestDNSProvider_concurrentGetDNSEntries(t *testing.T) { + client := &fakeClient{ + getInfoLatency: 50 * time.Millisecond, + setDNSEntriesLatency: 500 * time.Millisecond, + domainName: "lego.wtf", + } + + repo := domain.Repository{Client: client} + + p := &DNSProvider{ + config: NewDefaultConfig(), + repository: repo, + } + + var wg sync.WaitGroup + wg.Add(2) + + solve := func(domain1, suffix string, timeoutPresent, timeoutSolve, timeoutCleanup time.Duration) error { + time.Sleep(timeoutPresent) + + err := p.Present(domain1, "", "") + if err != nil { + return err + } + + time.Sleep(timeoutSolve) + + var found bool + for _, entry := range client.dnsEntries { + if strings.HasSuffix(entry.Name, suffix) { + found = true + } + } + if !found { + return fmt.Errorf("record %s not found: %v", suffix, client.dnsEntries) + } + + time.Sleep(timeoutCleanup) + + return p.CleanUp(domain1, "", "") + } + + go func() { + defer wg.Done() + err := solve("bar.lego.wtf", ".bar", 500*time.Millisecond, 100*time.Millisecond, 100*time.Millisecond) + require.NoError(t, err) + }() + + go func() { + defer wg.Done() + err := solve("foo.lego.wtf", ".foo", 500*time.Millisecond, 200*time.Millisecond, 100*time.Millisecond) + require.NoError(t, err) + }() + + wg.Wait() + + assert.Empty(t, client.dnsEntries) +} + +func TestDNSProvider_concurrentAddDNSEntry(t *testing.T) { + client := &fakeClient{ + domainName: "lego.wtf", + } + repo := domain.Repository{Client: client} + + p := &DNSProvider{ + config: NewDefaultConfig(), + repository: repo, + } + + var wg sync.WaitGroup + wg.Add(2) + + solve := func(domain1 string, timeoutPresent, timeoutCleanup time.Duration) error { + time.Sleep(timeoutPresent) + err := p.Present(domain1, "", "") + if err != nil { + return err + } + + time.Sleep(timeoutCleanup) + return p.CleanUp(domain1, "", "") + } + + go func() { + defer wg.Done() + err := solve("bar.lego.wtf", 550*time.Millisecond, 500*time.Millisecond) + require.NoError(t, err) + }() + + go func() { + defer wg.Done() + err := solve("foo.lego.wtf", 500*time.Millisecond, 100*time.Millisecond) + require.NoError(t, err) + }() + + wg.Wait() + + assert.Empty(t, client.dnsEntries) +} + func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -172,7 +275,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/ultradns/ultradns.go b/providers/dns/ultradns/ultradns.go index da76c56f4..5e2811959 100644 --- a/providers/dns/ultradns/ultradns.go +++ b/providers/dns/ultradns/ultradns.go @@ -4,13 +4,10 @@ package ultradns import ( "errors" "fmt" - "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" "github.com/ultradns/ultradns-go-sdk/pkg/client" "github.com/ultradns/ultradns-go-sdk/pkg/record" "github.com/ultradns/ultradns-go-sdk/pkg/rrset" @@ -27,12 +24,12 @@ const ( EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + + // Default variables names. + defaultEndpoint = "https://api.ultradns.com/" + defaultUserAgent = "lego-provider-ultradns" ) -const defaultEndpoint = "https://api.ultradns.com/" - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config @@ -54,7 +51,7 @@ type Config struct { func NewDefaultConfig() *Config { return &Config{ Endpoint: env.GetOrDefaultString(EnvEndpoint, defaultEndpoint), - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + TTL: env.GetOrDefaultInt(EnvTTL, 120), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 4*time.Second), } @@ -86,7 +83,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { Username: config.Username, Password: config.Password, HostURL: config.Endpoint, - UserAgent: useragent.Get(), + UserAgent: defaultUserAgent, } uClient, err := client.NewClient(ultraConfig) @@ -122,7 +119,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { RecordType: "TXT", } - resp, _, _ := recordService.Read(rrSetKeyData) + res, _, _ := recordService.Read(rrSetKeyData) rrSetData := &rrset.RRSet{ OwnerName: info.EffectiveFQDN, @@ -131,12 +128,11 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { RData: []string{info.Value}, } - if resp != nil && resp.StatusCode == http.StatusOK { + if res != nil && res.StatusCode == 200 { _, err = recordService.Update(rrSetKeyData, rrSetData) } else { _, err = recordService.Create(rrSetKeyData, rrSetData) } - if err != nil { return fmt.Errorf("ultradns: %w", err) } diff --git a/providers/dns/ultradns/ultradns.toml b/providers/dns/ultradns/ultradns.toml index 4c3dbbe72..3db63fe7a 100644 --- a/providers/dns/ultradns/ultradns.toml +++ b/providers/dns/ultradns/ultradns.toml @@ -7,7 +7,7 @@ Since = "v4.10.0" Example = ''' ULTRADNS_USERNAME=username \ ULTRADNS_PASSWORD=password \ -lego --dns ultradns -d '*.example.com' -d example.com run +lego --email you@example.com --dns ultradns --domains my.example.org run ''' [Configuration] @@ -16,9 +16,9 @@ lego --dns ultradns -d '*.example.com' -d example.com run ULTRADNS_PASSWORD = "API Password" [Configuration.Additional] ULTRADNS_ENDPOINT = "API endpoint URL, defaults to https://api.ultradns.com/" - ULTRADNS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - ULTRADNS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 4)" - ULTRADNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" + ULTRADNS_TTL = "The TTL of the TXT record used for the DNS challenge" + ULTRADNS_POLLING_INTERVAL = "Time between DNS propagation check" + ULTRADNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" [Links] API = "https://ultra-portalstatic.ultradns.com/static/docs/REST-API_User_Guide.pdf" diff --git a/providers/dns/ultradns/ultradns_test.go b/providers/dns/ultradns/ultradns_test.go index 464bc51cd..e07b6a524 100644 --- a/providers/dns/ultradns/ultradns_test.go +++ b/providers/dns/ultradns/ultradns_test.go @@ -134,26 +134,24 @@ func TestNewDNSProviderConfig(t *testing.T) { }, { desc: "missing credentials", - expected: "ultradns: Missing required parameters: [ username, password ]", + expected: "ultradns: config validation failure: username is missing", }, { desc: "missing username", username: "", password: "api_password", - expected: "ultradns: Missing required parameters: [ username ]", + expected: "ultradns: config validation failure: username is missing", }, { desc: "missing password", username: "api_username", password: "", - expected: "ultradns: Missing required parameters: [ password ]", + expected: "ultradns: config validation failure: password is missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - envTest.ClearEnv() - config := NewDefaultConfig() config.Username = test.username config.Password = test.password @@ -177,7 +175,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/uniteddomains/uniteddomains.go b/providers/dns/uniteddomains/uniteddomains.go deleted file mode 100644 index 683cab1fe..000000000 --- a/providers/dns/uniteddomains/uniteddomains.go +++ /dev/null @@ -1,105 +0,0 @@ -// Package uniteddomains implements a DNS provider for solving the DNS-01 challenge using United-Domains. -package uniteddomains - -import ( - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/ionos" -) - -// Environment variables names. -const ( - envNamespace = "UNITEDDOMAINS_" - - EnvAPIKey = envNamespace + "API_KEY" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -const defaultBaseURL = "https://dnsapi.united-domains.de/dns" - -const minTTL = 300 - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config = ionos.Config - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, minTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 15*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - prv challenge.ProviderTimeout -} - -// NewDNSProvider returns a DNSProvider instance configured for United-Domains. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIKey) - if err != nil { - return nil, fmt.Errorf("uniteddomains: %w", err) - } - - config := NewDefaultConfig() - config.APIKey = values[EnvAPIKey] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for United-Domains. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("uniteddomains: the configuration of the DNS provider is nil") - } - - provider, err := ionos.NewDNSProviderConfig(config, defaultBaseURL) - if err != nil { - return nil, fmt.Errorf("uniteddomains: %w", err) - } - - return &DNSProvider{prv: provider}, nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.prv.Timeout() -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - err := d.prv.Present(domain, token, keyAuth) - if err != nil { - return fmt.Errorf("uniteddomains: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - err := d.prv.CleanUp(domain, token, keyAuth) - if err != nil { - return fmt.Errorf("uniteddomains: %w", err) - } - - return nil -} diff --git a/providers/dns/uniteddomains/uniteddomains.toml b/providers/dns/uniteddomains/uniteddomains.toml deleted file mode 100644 index fe8b9e574..000000000 --- a/providers/dns/uniteddomains/uniteddomains.toml +++ /dev/null @@ -1,22 +0,0 @@ -Name = "United-Domains" -Description = '''''' -URL = "https://www.united-domains.de/" -Code = "uniteddomains" -Since = "v4.29.0" - -Example = ''' -UNITEDDOMAINS_API_KEY=xxxxxxxx \ -lego --dns uniteddomains -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - UNITEDDOMAINS_API_KEY = "API key `.` https://www.united-domains.de/help/faq-article/getting-started-with-the-united-domains-dns-api/" - [Configuration.Additional] - UNITEDDOMAINS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - UNITEDDOMAINS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 900)" - UNITEDDOMAINS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - UNITEDDOMAINS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://www.united-domains.de/dns-apidoc/" diff --git a/providers/dns/uniteddomains/uniteddomains_test.go b/providers/dns/uniteddomains/uniteddomains_test.go deleted file mode 100644 index 93afb01ab..000000000 --- a/providers/dns/uniteddomains/uniteddomains_test.go +++ /dev/null @@ -1,126 +0,0 @@ -package uniteddomains - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAPIKey: "123", - }, - }, - { - desc: "missing credentials", - envVars: map[string]string{ - EnvAPIKey: "", - }, - expected: "uniteddomains: some credentials information are missing: UNITEDDOMAINS_API_KEY", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.prv) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiKey string - tll int - expected string - }{ - { - desc: "success", - apiKey: "123", - tll: minTTL, - }, - { - desc: "missing credentials", - tll: minTTL, - expected: "uniteddomains: credentials missing", - }, - { - desc: "invalid TTL", - apiKey: "123", - tll: 30, - expected: "uniteddomains: invalid TTL, TTL (30) must be greater than 300", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIKey = test.apiKey - config.TTL = test.tll - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.prv) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/variomedia/internal/client.go b/providers/dns/variomedia/internal/client.go index 0e4ef9518..4a671e88e 100644 --- a/providers/dns/variomedia/internal/client.go +++ b/providers/dns/variomedia/internal/client.go @@ -38,7 +38,7 @@ func NewClient(apiToken string) *Client { // CreateDNSRecord creates a new DNS entry. // https://api.variomedia.de/docs/dns-records.html#erstellen -func (c *Client) CreateDNSRecord(ctx context.Context, record DNSRecord) (*CreateDNSRecordResponse, error) { +func (c Client) CreateDNSRecord(ctx context.Context, record DNSRecord) (*CreateDNSRecordResponse, error) { endpoint := c.baseURL.JoinPath("dns-records") data := CreateDNSRecordRequest{Data: Data{ @@ -52,7 +52,6 @@ func (c *Client) CreateDNSRecord(ctx context.Context, record DNSRecord) (*Create } var result CreateDNSRecordResponse - err = c.do(req, &result) if err != nil { return nil, err @@ -63,7 +62,7 @@ func (c *Client) CreateDNSRecord(ctx context.Context, record DNSRecord) (*Create // DeleteDNSRecord deletes a DNS record. // https://api.variomedia.de/docs/dns-records.html#l%C3%B6schen -func (c *Client) DeleteDNSRecord(ctx context.Context, id string) (*DeleteRecordResponse, error) { +func (c Client) DeleteDNSRecord(ctx context.Context, id string) (*DeleteRecordResponse, error) { endpoint := c.baseURL.JoinPath("dns-records", id) req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) @@ -72,7 +71,6 @@ func (c *Client) DeleteDNSRecord(ctx context.Context, id string) (*DeleteRecordR } var result DeleteRecordResponse - err = c.do(req, &result) if err != nil { return nil, err @@ -83,7 +81,7 @@ func (c *Client) DeleteDNSRecord(ctx context.Context, id string) (*DeleteRecordR // GetJob returns a single job based on its ID. // https://api.variomedia.de/docs/job-queue.html -func (c *Client) GetJob(ctx context.Context, id string) (*GetJobResponse, error) { +func (c Client) GetJob(ctx context.Context, id string) (*GetJobResponse, error) { endpoint := c.baseURL.JoinPath("queue-jobs", id) req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) @@ -92,7 +90,6 @@ func (c *Client) GetJob(ctx context.Context, id string) (*GetJobResponse, error) } var result GetJobResponse - err = c.do(req, &result) if err != nil { return nil, err @@ -101,7 +98,7 @@ func (c *Client) GetJob(ctx context.Context, id string) (*GetJobResponse, error) return &result, nil } -func (c *Client) do(req *http.Request, data any) error { +func (c Client) do(req *http.Request, data any) error { req.Header.Set(authorizationHeader, "token "+c.apiToken) resp, err := c.HTTPClient.Do(req) @@ -156,7 +153,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) var errAPI APIError - err := json.Unmarshal(raw, &errAPI) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/variomedia/internal/client_test.go b/providers/dns/variomedia/internal/client_test.go index 24778bdaf..c0017f24a 100644 --- a/providers/dns/variomedia/internal/client_test.go +++ b/providers/dns/variomedia/internal/client_test.go @@ -1,37 +1,68 @@ package internal import ( + "context" + "fmt" + "io" + "net/http" "net/http/httptest" "net/url" + "os" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("secret") - client.baseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() - return client, nil - }, - servermock.CheckHeader(). - WithAccept("application/vnd.variomedia.v1+json"). - WithAuthorization("token secret")) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient("secret") + client.baseURL, _ = url.Parse(server.URL) + + return client, mux +} + +func mockHandler(method string, filename string) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("invalid method, got %s want %s", req.Method, method), http.StatusBadRequest) + return + } + + filename = "./fixtures/" + filename + statusCode := http.StatusOK + + if req.Header.Get(authorizationHeader) != "token secret" { + statusCode = http.StatusUnauthorized + filename = "./fixtures/error.json" + } + + rw.WriteHeader(statusCode) + + file, err := os.Open(filename) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + } } func TestClient_CreateDNSRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /dns-records", - servermock.ResponseFromFixture("POST_dns-records.json"), - servermock.CheckHeader(). - WithContentType("application/vnd.api+json"), - servermock.CheckRequestJSONBody(`{"data":{"type":"dns-record","attributes":{"record_type":"TXT","name":"_acme-challenge","domain":"example.com","data":"test","ttl":300}}}`)). - Build(t) + client, mux := setupTest(t) + + mux.HandleFunc("/dns-records", mockHandler(http.MethodPost, "POST_dns-records.json")) record := DNSRecord{ RecordType: "TXT", @@ -41,7 +72,7 @@ func TestClient_CreateDNSRecord(t *testing.T) { TTL: 300, } - resp, err := client.CreateDNSRecord(t.Context(), record) + resp, err := client.CreateDNSRecord(context.Background(), record) require.NoError(t, err) expected := &CreateDNSRecordResponse{ @@ -77,12 +108,11 @@ func TestClient_CreateDNSRecord(t *testing.T) { } func TestClient_DeleteDNSRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /dns-records/test", - servermock.ResponseFromFixture("DELETE_dns-records_pending.json")). - Build(t) + client, mux := setupTest(t) - resp, err := client.DeleteDNSRecord(t.Context(), "test") + mux.HandleFunc("/dns-records/test", mockHandler(http.MethodDelete, "DELETE_dns-records_pending.json")) + + resp, err := client.DeleteDNSRecord(context.Background(), "test") require.NoError(t, err) expected := &DeleteRecordResponse{ @@ -113,12 +143,11 @@ func TestClient_DeleteDNSRecord(t *testing.T) { } func TestClient_GetJob(t *testing.T) { - client := mockBuilder(). - Route("GET /queue-jobs/test", - servermock.ResponseFromFixture("GET_queue-jobs.json")). - Build(t) + client, mux := setupTest(t) - resp, err := client.GetJob(t.Context(), "test") + mux.HandleFunc("/queue-jobs/test", mockHandler(http.MethodGet, "GET_queue-jobs.json")) + + resp, err := client.GetJob(context.Background(), "test") require.NoError(t, err) expected := &GetJobResponse{ diff --git a/providers/dns/variomedia/variomedia.go b/providers/dns/variomedia/variomedia.go index 2d12fd975..4a7d0e901 100644 --- a/providers/dns/variomedia/variomedia.go +++ b/providers/dns/variomedia/variomedia.go @@ -10,13 +10,10 @@ import ( "sync" "time" - "github.com/cenkalti/backoff/v5" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/platform/wait" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/variomedia/internal" ) @@ -33,8 +30,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIToken string @@ -93,8 +88,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, client: client, @@ -165,7 +158,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() - if !ok { return fmt.Errorf("variomedia: unknown record ID for '%s'", info.EffectiveFQDN) } @@ -180,30 +172,18 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("variomedia: %w", err) } - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - return nil } -func (d *DNSProvider) waitJob(ctx context.Context, domain, id string) error { - return wait.Retry(ctx, - func() error { - result, err := d.client.GetJob(ctx, id) - if err != nil { - return fmt.Errorf("apply change on %s: %w", domain, err) - } +func (d *DNSProvider) waitJob(ctx context.Context, domain string, id string) error { + return wait.For("variomedia: apply change on "+domain, d.config.PropagationTimeout, d.config.PollingInterval, func() (bool, error) { + result, err := d.client.GetJob(ctx, id) + if err != nil { + return false, err + } - log.Infof("variomedia: [%s] %s: %s %s", domain, result.Data.ID, result.Data.Attributes.JobType, result.Data.Attributes.Status) + log.Infof("variomedia: [%s] %s: %s %s", domain, result.Data.ID, result.Data.Attributes.JobType, result.Data.Attributes.Status) - if result.Data.Attributes.Status != "done" { - return fmt.Errorf("apply change on %s: status: %s", domain, result.Data.Attributes.Status) - } - - return nil - }, - backoff.WithBackOff(backoff.NewConstantBackOff(d.config.PollingInterval)), - backoff.WithMaxElapsedTime(d.config.PropagationTimeout), - ) + return result.Data.Attributes.Status == "done", nil + }) } diff --git a/providers/dns/variomedia/variomedia.toml b/providers/dns/variomedia/variomedia.toml index 8390d1922..a75918954 100644 --- a/providers/dns/variomedia/variomedia.toml +++ b/providers/dns/variomedia/variomedia.toml @@ -6,18 +6,19 @@ Since = "v4.8.0" Example = ''' VARIOMEDIA_API_TOKEN=xxxx \ -lego --dns variomedia -d '*.example.com' -d example.com run +lego --email you@example.com --dns variomedia --domains my.example.org run ''' [Configuration] [Configuration.Credentials] - VARIOMEDIA_API_TOKEN = "API token" + VARIOMEDIA_API_TOKEN = "API token" [Configuration.Additional] - VARIOMEDIA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - VARIOMEDIA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - VARIOMEDIA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - VARIOMEDIA_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 60)" - VARIOMEDIA_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + VARIOMEDIA_POLLING_INTERVAL = "Time between DNS propagation check" + VARIOMEDIA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + VARIOMEDIA_TTL = "The TTL of the TXT record used for the DNS challenge" + DODE_SEQUENCE_INTERVAL = "Time between sequential requests" + VARIOMEDIA_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://api.variomedia.de/docs/dns-records.html" + diff --git a/providers/dns/variomedia/variomedia_test.go b/providers/dns/variomedia/variomedia_test.go index 552419fd0..305646070 100644 --- a/providers/dns/variomedia/variomedia_test.go +++ b/providers/dns/variomedia/variomedia_test.go @@ -33,7 +33,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -92,7 +91,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -106,7 +104,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/vegadns/fixtures/create_record.json b/providers/dns/vegadns/fixtures/create_record.json deleted file mode 100644 index 2199130b9..000000000 --- a/providers/dns/vegadns/fixtures/create_record.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "status": "ok", - "record": { - "name": "_acme-challenge.example.com", - "value": "my_challenge", - "record_type": "TXT", - "ttl": 3600, - "record_id": 3, - "location_id": null, - "domain_id": 1 - } -} diff --git a/providers/dns/vegadns/fixtures/record_delete.json b/providers/dns/vegadns/fixtures/record_delete.json deleted file mode 100644 index bc4e01029..000000000 --- a/providers/dns/vegadns/fixtures/record_delete.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "status": "ok" -} diff --git a/providers/dns/vegadns/fixtures/records.json b/providers/dns/vegadns/fixtures/records.json deleted file mode 100644 index 9fa41ce7a..000000000 --- a/providers/dns/vegadns/fixtures/records.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "status": "ok", - "total_records": 2, - "domain": { - "status": "active", - "domain": "example.com", - "owner_id": 0, - "domain_id": 1 - }, - "records": [ - { - "retry": "2048", - "minimum": "2560", - "refresh": "16384", - "email": "hostmaster.example.com", - "record_type": "SOA", - "expire": "1048576", - "ttl": 86400, - "record_id": 1, - "nameserver": "ns1.example.com", - "domain_id": 1, - "serial": "" - }, - { - "name": "example.com", - "value": "ns1.example.com", - "record_type": "NS", - "ttl": 3600, - "record_id": 2, - "location_id": null, - "domain_id": 1 - }, - { - "name": "_acme-challenge.example.com", - "value": "my_challenge", - "record_type": "TXT", - "ttl": 3600, - "record_id": 3, - "location_id": null, - "domain_id": 1 - } - ] -} diff --git a/providers/dns/vegadns/fixtures/token.json b/providers/dns/vegadns/fixtures/token.json deleted file mode 100644 index 39ab1a4a9..000000000 --- a/providers/dns/vegadns/fixtures/token.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "access_token": "699dd4ff-e381-46b8-8bf8-5de49dd56c1f", - "token_type": "bearer", - "expires_in": 3600 -} diff --git a/providers/dns/vegadns/vegadns.go b/providers/dns/vegadns/vegadns.go index 9f1f189c3..0da11ef31 100644 --- a/providers/dns/vegadns/vegadns.go +++ b/providers/dns/vegadns/vegadns.go @@ -2,17 +2,13 @@ package vegadns import ( - "context" "errors" "fmt" - "net/http" "time" - "github.com/go-acme/lego/v4/challenge" + vegaClient "github.com/OpenDNS/vegadns2client" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/nrdcg/vegadns" ) // Environment variables names. @@ -26,21 +22,16 @@ const ( EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { - BaseURL string - APIKey string - APISecret string - + BaseURL string + APIKey string + APISecret string PropagationTimeout time.Duration PollingInterval time.Duration TTL int - HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. @@ -48,17 +39,14 @@ func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 10), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 12*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, time.Minute), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 1*time.Minute), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config - client *vegadns.Client + client vegaClient.VegaDNSClient } // NewDNSProvider returns a DNSProvider instance configured for VegaDNS. @@ -84,21 +72,11 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("vegadns: the configuration of the DNS provider is nil") } - if config.HTTPClient == nil { - config.HTTPClient = &http.Client{Timeout: 30 * time.Second} - } + vega := vegaClient.NewVegaDNSClient(config.BaseURL) + vega.APIKey = config.APIKey + vega.APISecret = config.APISecret - config.HTTPClient = clientdebug.Wrap(config.HTTPClient) - - client, err := vegadns.NewClient(config.BaseURL, - vegadns.WithOAuth(config.APIKey, config.APISecret), - vegadns.WithHTTPClient(config.HTTPClient), - ) - if err != nil { - return nil, fmt.Errorf("vegadns: %w", err) - } - - return &DNSProvider{client: client, config: config}, nil + return &DNSProvider{client: vega, config: config}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. @@ -109,71 +87,39 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) - domainID, err := d.findDomainID(ctx, info.EffectiveFQDN) + _, domainID, err := d.client.GetAuthZone(info.EffectiveFQDN) if err != nil { - return fmt.Errorf("vegadns: find domain ID for %s: %w", info.EffectiveFQDN, err) + return fmt.Errorf("vegadns: can't find Authoritative Zone for %s in Present: %w", info.EffectiveFQDN, err) } - err = d.client.CreateTXTRecord(ctx, domainID, dns01.UnFqdn(info.EffectiveFQDN), info.Value, d.config.TTL) + err = d.client.CreateTXT(domainID, info.EffectiveFQDN, info.Value, d.config.TTL) if err != nil { - return fmt.Errorf("vegadns: create TXT record: %w", err) + return fmt.Errorf("vegadns: %w", err) } - return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) - domainID, err := d.findDomainID(ctx, info.EffectiveFQDN) + _, domainID, err := d.client.GetAuthZone(info.EffectiveFQDN) if err != nil { - return fmt.Errorf("vegadns: find domain ID for %s: %w", info.EffectiveFQDN, err) + return fmt.Errorf("vegadns: can't find Authoritative Zone for %s in CleanUp: %w", info.EffectiveFQDN, err) } - recordID, err := d.findRecordID(ctx, domainID, dns01.UnFqdn(info.EffectiveFQDN)) + txt := dns01.UnFqdn(info.EffectiveFQDN) + + recordID, err := d.client.GetRecordID(domainID, txt, "TXT") if err != nil { - return fmt.Errorf("vegadns: find record ID for %d: %w", domainID, err) + return fmt.Errorf("vegadns: couldn't get Record ID in CleanUp: %w", err) } - err = d.client.DeleteRecord(ctx, recordID) + err = d.client.DeleteRecord(recordID) if err != nil { - return fmt.Errorf("vegadns: delete record: %w", err) + return fmt.Errorf("vegadns: %w", err) } - return nil } - -func (d *DNSProvider) findDomainID(ctx context.Context, fqdn string) (int, error) { - for host := range dns01.UnFqdnDomainsSeq(fqdn) { - id, err := d.client.GetDomainID(ctx, host) - if err != nil { - continue - } - - return id, nil - } - - return 0, errors.New("domain not found") -} - -func (d *DNSProvider) findRecordID(ctx context.Context, domainID int, name string) (int, error) { - records, err := d.client.GetRecords(ctx, domainID) - if err != nil { - return 0, fmt.Errorf("get records: %w", err) - } - - for _, r := range records { - if r.Name == name && r.RecordType == "TXT" { - return r.RecordID, nil - } - } - - return 0, errors.New("record not found") -} diff --git a/providers/dns/vegadns/vegadns.toml b/providers/dns/vegadns/vegadns.toml index d01490f55..e1a7cc713 100644 --- a/providers/dns/vegadns/vegadns.toml +++ b/providers/dns/vegadns/vegadns.toml @@ -12,9 +12,9 @@ Example = '''''' SECRET_VEGADNS_SECRET = "API secret" VEGADNS_URL = "API endpoint URL" [Configuration.Additional] - VEGADNS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 60)" - VEGADNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 720)" - VEGADNS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 10)" + VEGADNS_POLLING_INTERVAL = "Time between DNS propagation check" + VEGADNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + VEGADNS_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://github.com/shupp/VegaDNS-API" diff --git a/providers/dns/vegadns/vegadns_mock_test.go b/providers/dns/vegadns/vegadns_mock_test.go new file mode 100644 index 000000000..5a705e092 --- /dev/null +++ b/providers/dns/vegadns/vegadns_mock_test.go @@ -0,0 +1,85 @@ +package vegadns + +const tokenResponseMock = ` +{ + "access_token":"699dd4ff-e381-46b8-8bf8-5de49dd56c1f", + "token_type":"bearer", + "expires_in":3600 +} +` + +const domainsResponseMock = ` +{ + "domains":[ + { + "domain_id":1, + "domain":"example.com", + "status":"active", + "owner_id":0 + } + ] +} +` + +const recordsResponseMock = ` +{ + "status":"ok", + "total_records":2, + "domain":{ + "status":"active", + "domain":"example.com", + "owner_id":0, + "domain_id":1 + }, + "records":[ + { + "retry":"2048", + "minimum":"2560", + "refresh":"16384", + "email":"hostmaster.example.com", + "record_type":"SOA", + "expire":"1048576", + "ttl":86400, + "record_id":1, + "nameserver":"ns1.example.com", + "domain_id":1, + "serial":"" + }, + { + "name":"example.com", + "value":"ns1.example.com", + "record_type":"NS", + "ttl":3600, + "record_id":2, + "location_id":null, + "domain_id":1 + }, + { + "name":"_acme-challenge.example.com", + "value":"my_challenge", + "record_type":"TXT", + "ttl":3600, + "record_id":3, + "location_id":null, + "domain_id":1 + } + ] +} +` + +const recordCreatedResponseMock = ` +{ + "status":"ok", + "record":{ + "name":"_acme-challenge.example.com", + "value":"my_challenge", + "record_type":"TXT", + "ttl":3600, + "record_id":3, + "location_id":null, + "domain_id":1 + } +} +` + +const recordDeletedResponseMock = `{"status": "ok"}` diff --git a/providers/dns/vegadns/vegadns_test.go b/providers/dns/vegadns/vegadns_test.go index edcd2c60d..60f614c3b 100644 --- a/providers/dns/vegadns/vegadns_test.go +++ b/providers/dns/vegadns/vegadns_test.go @@ -8,7 +8,6 @@ import ( "time" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -19,7 +18,6 @@ var envTest = tester.NewEnvTest(EnvKey, EnvSecret, EnvURL) func TestNewDNSProvider_Fail(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() _, err := NewDNSProvider() @@ -28,10 +26,12 @@ func TestNewDNSProvider_Fail(t *testing.T) { func TestDNSProvider_TimeoutSuccess(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() - provider := mockBuilder().Build(t) + setupTest(t, muxSuccess()) + + provider, err := NewDNSProvider() + require.NoError(t, err) timeout, interval := provider.Timeout() assert.Equal(t, 12*time.Minute, timeout) @@ -42,51 +42,35 @@ func TestDNSProvider_Present(t *testing.T) { testCases := []struct { desc string handler http.Handler - builder *servermock.Builder[*DNSProvider] expectedError string }{ { - desc: "success", - builder: mockBuilder(). - Route("POST /1.0/token", - servermock.ResponseFromFixture("token.json")). - Route("GET /1.0/domains", getDomainHandler()). - Route("POST /1.0/records", - servermock.ResponseFromFixture("create_record.json"). - WithStatusCode(http.StatusCreated)), + desc: "Success", + handler: muxSuccess(), }, { - desc: "fail to find the zone", - builder: mockBuilder(). - Route("POST /1.0/token", - servermock.ResponseFromFixture("token.json")). - Route("GET /1.0/domains", - servermock.Noop(). - WithStatusCode(http.StatusNotFound)), - expectedError: "vegadns: find domain ID for _acme-challenge.example.com.: domain not found", + desc: "FailToFindZone", + handler: muxFailToFindZone(), + expectedError: "vegadns: can't find Authoritative Zone for _acme-challenge.example.com. in Present: Unable to find auth zone for fqdn _acme-challenge.example.com", }, { - desc: "fail to create TXT record", - builder: mockBuilder(). - Route("POST /1.0/token", - servermock.ResponseFromFixture("token.json")). - Route("GET /1.0/domains", getDomainHandler()). - Route("POST /1.0/records", - servermock.Noop(). - WithStatusCode(http.StatusBadRequest)), - expectedError: "vegadns: create TXT record: bad answer from VegaDNS (code: 400, message: )", + desc: "FailToCreateTXT", + handler: muxFailToCreateTXT(), + expectedError: "vegadns: Got bad answer from VegaDNS on CreateTXT. Code: 400. Message: ", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() - provider := test.builder.Build(t) + setupTest(t, test.handler) - err := provider.Present(testDomain, "token", "keyAuth") + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(testDomain, "token", "keyAuth") if test.expectedError == "" { assert.NoError(t, err) } else { @@ -99,54 +83,36 @@ func TestDNSProvider_Present(t *testing.T) { func TestDNSProvider_CleanUp(t *testing.T) { testCases := []struct { desc string - builder *servermock.Builder[*DNSProvider] + handler http.Handler expectedError string }{ { - desc: "success", - builder: mockBuilder(). - Route("POST /1.0/token", - servermock.ResponseFromFixture("token.json")). - Route("GET /1.0/domains", getDomainHandler()). - Route("GET /1.0/records", - servermock.ResponseFromFixture("records.json"), - servermock.CheckQueryParameter().With("domain_id", "1")). - Route("DELETE /1.0/records/3", - servermock.ResponseFromFixture("record_delete.json")), + desc: "Success", + handler: muxSuccess(), }, { - desc: "fail to find the zone", - builder: mockBuilder(). - Route("POST /1.0/token", - servermock.ResponseFromFixture("token.json")). - Route("GET /1.0/domains", - servermock.Noop(). - WithStatusCode(http.StatusNotFound)), - expectedError: "vegadns: find domain ID for _acme-challenge.example.com.: domain not found", + desc: "FailToFindZone", + handler: muxFailToFindZone(), + expectedError: "vegadns: can't find Authoritative Zone for _acme-challenge.example.com. in CleanUp: Unable to find auth zone for fqdn _acme-challenge.example.com", }, { - desc: "fail to get record ID", - builder: mockBuilder(). - Route("POST /1.0/token", - servermock.ResponseFromFixture("token.json")). - Route("GET /1.0/domains", getDomainHandler()). - Route("GET /1.0/records", - servermock.Noop(). - WithStatusCode(http.StatusNotFound), - servermock.CheckQueryParameter().With("domain_id", "1")), - expectedError: "vegadns: find record ID for 1: get records: bad answer from VegaDNS (code: 404, message: )", + desc: "FailToGetRecordID", + handler: muxFailToGetRecordID(), + expectedError: "vegadns: couldn't get Record ID in CleanUp: Got bad answer from VegaDNS on GetRecordID. Code: 404. Message: ", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() - provider := test.builder.Build(t) + setupTest(t, test.handler) - err := provider.CleanUp(testDomain, "token", "keyAuth") + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp(testDomain, "token", "keyAuth") if test.expectedError == "" { assert.NoError(t, err) } else { @@ -156,37 +122,163 @@ func TestDNSProvider_CleanUp(t *testing.T) { } } -func getDomainHandler() http.HandlerFunc { - return func(rw http.ResponseWriter, req *http.Request) { - if req.URL.Query().Get("search") == testDomain { - fmt.Fprint(rw, ` -{ - "domains":[ - { - "domain_id":1, - "domain":"example.com", - "status":"active", - "owner_id":0 - } - ] -} -`) +func muxSuccess() *http.ServeMux { + mux := http.NewServeMux() + mux.HandleFunc("/1.0/token", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, tokenResponseMock) return } + w.WriteHeader(http.StatusBadRequest) + }) - rw.WriteHeader(http.StatusNotFound) - } + mux.HandleFunc("/1.0/domains", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("search") == "example.com" { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, domainsResponseMock) + return + } + w.WriteHeader(http.StatusNotFound) + }) + + mux.HandleFunc("/1.0/records", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + if r.URL.Query().Get("domain_id") == "1" { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, recordsResponseMock) + return + } + w.WriteHeader(http.StatusNotFound) + return + case http.MethodPost: + w.WriteHeader(http.StatusCreated) + fmt.Fprint(w, recordCreatedResponseMock) + return + } + w.WriteHeader(http.StatusBadRequest) + }) + + mux.HandleFunc("/1.0/records/3", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodDelete { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, recordDeletedResponseMock) + return + } + w.WriteHeader(http.StatusBadRequest) + }) + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + fmt.Printf("Not Found for Request: (%+v)\n\n", r) + }) + + return mux } -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) { - envTest.Apply(map[string]string{ - EnvKey: "key", - EnvSecret: "secret", - EnvURL: server.URL, - }) +func muxFailToFindZone() *http.ServeMux { + mux := http.NewServeMux() - return NewDNSProvider() + mux.HandleFunc("/1.0/token", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, tokenResponseMock) + return + } + w.WriteHeader(http.StatusBadRequest) + }) + + mux.HandleFunc("/1.0/domains", func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + }) + + return mux +} + +func muxFailToCreateTXT() *http.ServeMux { + mux := http.NewServeMux() + + mux.HandleFunc("/1.0/token", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, tokenResponseMock) + return + } + w.WriteHeader(http.StatusBadRequest) + }) + + mux.HandleFunc("/1.0/domains", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("search") == testDomain { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, domainsResponseMock) + return + } + w.WriteHeader(http.StatusNotFound) + }) + + mux.HandleFunc("/1.0/records", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + if r.URL.Query().Get("domain_id") == "1" { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, recordsResponseMock) + return + } + w.WriteHeader(http.StatusNotFound) + return + case http.MethodPost: + w.WriteHeader(http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusBadRequest) + }) + + return mux +} + +func muxFailToGetRecordID() *http.ServeMux { + mux := http.NewServeMux() + + mux.HandleFunc("/1.0/token", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, tokenResponseMock) + return + } + w.WriteHeader(http.StatusBadRequest) + }) + + mux.HandleFunc("/1.0/domains", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("search") == testDomain { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, domainsResponseMock) + return + } + w.WriteHeader(http.StatusNotFound) + }) + + mux.HandleFunc("/1.0/records", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + w.WriteHeader(http.StatusNotFound) + return + } + w.WriteHeader(http.StatusBadRequest) + }) + + return mux +} + +func setupTest(t *testing.T, mux http.Handler) { + t.Helper() + + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + envTest.Apply(map[string]string{ + EnvKey: "key", + EnvSecret: "secret", + EnvURL: server.URL, }) } diff --git a/providers/dns/vercel/internal/client.go b/providers/dns/vercel/internal/client.go index 930f3543e..4bc59ba0c 100644 --- a/providers/dns/vercel/internal/client.go +++ b/providers/dns/vercel/internal/client.go @@ -51,7 +51,6 @@ func (c *Client) CreateRecord(ctx context.Context, zone string, record Record) ( } respData := &CreateRecordResponse{} - err = c.do(req, respData) if err != nil { return nil, err @@ -62,7 +61,7 @@ func (c *Client) CreateRecord(ctx context.Context, zone string, record Record) ( // DeleteRecord deletes a DNS record. // https://vercel.com/docs/rest-api#endpoints/dns/delete-a-dns-record -func (c *Client) DeleteRecord(ctx context.Context, zone, recordID string) error { +func (c *Client) DeleteRecord(ctx context.Context, zone string, recordID string) error { endpoint := c.baseURL.JoinPath("v2", "domains", dns01.UnFqdn(zone), "records", recordID) req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) @@ -136,7 +135,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) var response APIErrorResponse - err := json.Unmarshal(raw, &response) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/vercel/internal/client_test.go b/providers/dns/vercel/internal/client_test.go index eb5ee501d..771349b25 100644 --- a/providers/dns/vercel/internal/client_test.go +++ b/providers/dns/vercel/internal/client_test.go @@ -1,38 +1,72 @@ package internal import ( + "bytes" + "context" + "fmt" + "io" + "net/http" "net/http/httptest" "net/url" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient(OAuthStaticAccessToken(server.Client(), "secret"), "123") - client.baseURL, _ = url.Parse(server.URL) +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("Bearer secret")) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient(OAuthStaticAccessToken(server.Client(), "secret"), "123") + client.baseURL, _ = url.Parse(server.URL) + + return client, mux } func TestClient_CreateRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /v2/domains/example.com/records", - servermock.RawStringResponse(`{ + client, mux := setupTest(t) + + mux.HandleFunc("/v2/domains/example.com/records", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + + auth := req.Header.Get("Authorization") + if auth != "Bearer secret" { + http.Error(rw, fmt.Sprintf("invalid API token: %s", auth), http.StatusUnauthorized) + return + } + + teamID := req.URL.Query().Get("teamId") + if teamID != "123" { + http.Error(rw, fmt.Sprintf("invalid team ID: %s", teamID), http.StatusUnauthorized) + return + } + + reqBody, err := io.ReadAll(req.Body) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + expectedReqBody := `{"name":"_acme-challenge.example.com.","type":"TXT","value":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","ttl":60}` + assert.Equal(t, expectedReqBody, string(bytes.TrimSpace(reqBody))) + + rw.WriteHeader(http.StatusOK) + _, err = fmt.Fprintf(rw, `{ "uid": "9e2eab60-0ba5-4dff-b481-2999c9764b84", "updated": 1 - }`), - servermock.CheckRequestJSONBody(`{"name":"_acme-challenge.example.com.","type":"TXT","value":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","ttl":60}`), - servermock.CheckQueryParameter().Strict(). - With("teamId", "123")). - Build(t) + }`) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) record := Record{ Name: "_acme-challenge.example.com.", @@ -41,7 +75,7 @@ func TestClient_CreateRecord(t *testing.T) { TTL: 60, } - resp, err := client.CreateRecord(t.Context(), "example.com.", record) + resp, err := client.CreateRecord(context.Background(), "example.com.", record) require.NoError(t, err) expected := &CreateRecordResponse{ @@ -53,12 +87,28 @@ func TestClient_CreateRecord(t *testing.T) { } func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /v2/domains/example.com/records/1234567", nil, - servermock.CheckQueryParameter().Strict(). - With("teamId", "123")). - Build(t) + client, mux := setupTest(t) - err := client.DeleteRecord(t.Context(), "example.com.", "1234567") + mux.HandleFunc("/v2/domains/example.com/records/1234567", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodDelete { + http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) + return + } + auth := req.Header.Get("Authorization") + if auth != "Bearer secret" { + http.Error(rw, fmt.Sprintf("invalid API token: %s", auth), http.StatusUnauthorized) + return + } + + teamID := req.URL.Query().Get("teamId") + if teamID != "123" { + http.Error(rw, fmt.Sprintf("invalid team ID: %s", teamID), http.StatusUnauthorized) + return + } + + rw.WriteHeader(http.StatusOK) + }) + + err := client.DeleteRecord(context.Background(), "example.com.", "1234567") require.NoError(t, err) } diff --git a/providers/dns/vercel/vercel.go b/providers/dns/vercel/vercel.go index 965e3de12..491251fe5 100644 --- a/providers/dns/vercel/vercel.go +++ b/providers/dns/vercel/vercel.go @@ -9,10 +9,8 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/vercel/internal" ) @@ -29,8 +27,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { AuthToken string @@ -45,7 +41,7 @@ type Config struct { func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 60), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, time.Minute), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 60*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), @@ -87,12 +83,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("vercel: credentials missing") } - client := internal.NewClient( - clientdebug.Wrap( - internal.OAuthStaticAccessToken(config.HTTPClient, config.AuthToken), - ), - config.TeamID, - ) + client := internal.NewClient(internal.OAuthStaticAccessToken(config.HTTPClient, config.AuthToken), config.TeamID) return &DNSProvider{ config: config, @@ -148,7 +139,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() - if !ok { return fmt.Errorf("vercel: unknown record ID for '%s'", info.EffectiveFQDN) } diff --git a/providers/dns/vercel/vercel.toml b/providers/dns/vercel/vercel.toml index 4700d6d78..689caba6d 100644 --- a/providers/dns/vercel/vercel.toml +++ b/providers/dns/vercel/vercel.toml @@ -6,7 +6,7 @@ Since = "v4.7.0" Example = ''' VERCEL_API_TOKEN=xxxxxx \ -lego --dns vercel -d '*.example.com' -d example.com run +lego --email you@example.com --dns vercel --domains my.example.org run ''' [Configuration] @@ -14,10 +14,10 @@ lego --dns vercel -d '*.example.com' -d example.com run VERCEL_API_TOKEN = "Authentication token" [Configuration.Additional] VERCEL_TEAM_ID = "Team ID (ex: team_xxxxxxxxxxxxxxxxxxxxxxxx)" - VERCEL_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 5)" - VERCEL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - VERCEL_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" - VERCEL_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + VERCEL_POLLING_INTERVAL = "Time between DNS propagation check" + VERCEL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + VERCEL_TTL = "The TTL of the TXT record used for the DNS challenge" + VERCEL_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://vercel.com/docs/rest-api#endpoints/dns" diff --git a/providers/dns/vercel/vercel_test.go b/providers/dns/vercel/vercel_test.go index d4cf37904..6c19a4db5 100644 --- a/providers/dns/vercel/vercel_test.go +++ b/providers/dns/vercel/vercel_test.go @@ -36,7 +36,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -96,7 +95,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -110,7 +108,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/versio/fixtures/error_failToCreateTXT.json b/providers/dns/versio/fixtures/error_failToCreateTXT.json deleted file mode 100644 index 1e1784517..000000000 --- a/providers/dns/versio/fixtures/error_failToCreateTXT.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "error": { - "code": 400, - "message": "ProcessError|DNS record invalid type _acme-challenge.example.eu. TST" - } -} diff --git a/providers/dns/versio/fixtures/error_failToFindZone.json b/providers/dns/versio/fixtures/error_failToFindZone.json deleted file mode 100644 index 635b2bda1..000000000 --- a/providers/dns/versio/fixtures/error_failToFindZone.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "error": { - "code": 401, - "message": "ObjectDoesNotExist|Domain not found" - } -} diff --git a/providers/dns/versio/fixtures/token.json b/providers/dns/versio/fixtures/token.json deleted file mode 100644 index 0dc0dda25..000000000 --- a/providers/dns/versio/fixtures/token.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "access_token":"699dd4ff-e381-46b8-8bf8-5de49dd56c1f", - "token_type":"bearer", - "expires_in":3600 -} diff --git a/providers/dns/versio/internal/client.go b/providers/dns/versio/internal/client.go index 6a92cc958..6f70aacd2 100644 --- a/providers/dns/versio/internal/client.go +++ b/providers/dns/versio/internal/client.go @@ -26,7 +26,7 @@ type Client struct { } // NewClient creates a new Client. -func NewClient(username, password string) *Client { +func NewClient(username string, password string) *Client { baseURL, _ := url.Parse(DefaultBaseURL) return &Client{ @@ -48,7 +48,6 @@ func (c *Client) UpdateDomain(ctx context.Context, domain string, msg *DomainInf } respData := &DomainInfoResponse{} - err = c.do(req, respData) if err != nil { return nil, err @@ -72,7 +71,6 @@ func (c *Client) GetDomain(ctx context.Context, domain string) (*DomainInfoRespo } respData := &DomainInfoResponse{} - err = c.do(req, respData) if err != nil { return nil, err @@ -90,7 +88,6 @@ func (c *Client) do(req *http.Request, result any) error { if resp != nil { defer func() { _ = resp.Body.Close() }() } - if err != nil { return errutils.NewHTTPDoError(req, err) } @@ -143,7 +140,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) response := &ErrorResponse{} - err := json.Unmarshal(raw, response) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/versio/internal/client_test.go b/providers/dns/versio/internal/client_test.go index 8dfcb4ff8..f1015d28a 100644 --- a/providers/dns/versio/internal/client_test.go +++ b/providers/dns/versio/internal/client_test.go @@ -1,38 +1,64 @@ package internal import ( + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("user", "secret") - client.HTTPClient = server.Client() - client.BaseURL, _ = url.Parse(server.URL) +func setupTest(t *testing.T, pattern string, h http.HandlerFunc) *Client { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithBasicAuth("user", "secret")) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc(pattern, h) + + client := NewClient("user", "secret") + client.HTTPClient = server.Client() + client.BaseURL, _ = url.Parse(server.URL) + + return client +} + +func writeFixture(rw http.ResponseWriter, filename string) { + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, _ = io.Copy(rw, file) } func TestClient_GetDomain(t *testing.T) { - client := mockBuilder(). - Route("GET /domains/example.com", - servermock.ResponseFromFixture("get-domain.json"), - servermock.CheckQueryParameter().Strict(). - With("show_dns_records", "true")). - Build(t) + client := setupTest(t, "/domains/example.com", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } - records, err := client.GetDomain(t.Context(), "example.com") + auth := req.Header.Get("Authorization") + if auth != "Basic dXNlcjpzZWNyZXQ=" { + http.Error(rw, "invalid credentials: "+auth, http.StatusUnauthorized) + return + } + + writeFixture(rw, "get-domain.json") + }) + + records, err := client.GetDomain(context.Background(), "example.com") require.NoError(t, err) expected := &DomainInfoResponse{DomainInfo: DomainInfo{DNSRecords: []Record{ @@ -54,22 +80,36 @@ func TestClient_GetDomain(t *testing.T) { } func TestClient_GetDomain_error(t *testing.T) { - client := mockBuilder(). - Route("GET /domains/example.com", - servermock.ResponseFromFixture("get-domain-error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client := setupTest(t, "/domains/example.com", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } - _, err := client.GetDomain(t.Context(), "example.com") + rw.WriteHeader(http.StatusUnauthorized) + + writeFixture(rw, "get-domain-error.json") + }) + + _, err := client.GetDomain(context.Background(), "example.com") require.ErrorAs(t, err, &ErrorMessage{}) } func TestClient_UpdateDomain(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/example.com/update", - servermock.ResponseFromFixture("update-domain.json"), - servermock.CheckRequestJSONBodyFromFixture("update-domain-request.json")). - Build(t) + client := setupTest(t, "/domains/example.com/update", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + auth := req.Header.Get("Authorization") + if auth != "Basic dXNlcjpzZWNyZXQ=" { + http.Error(rw, "invalid credentials: "+auth, http.StatusUnauthorized) + return + } + + writeFixture(rw, "update-domain.json") + }) msg := &DomainInfo{DNSRecords: []Record{ {Type: "MX", Name: "example.com", Value: "fallback.axc.eu", Priority: 20, TTL: 3600}, @@ -86,7 +126,7 @@ func TestClient_UpdateDomain(t *testing.T) { {Type: "A", Name: "redirect.example.com", Value: "localhost", Priority: 10, TTL: 14400}, }} - records, err := client.UpdateDomain(t.Context(), "example.com", msg) + records, err := client.UpdateDomain(context.Background(), "example.com", msg) require.NoError(t, err) expected := &DomainInfoResponse{DomainInfo: DomainInfo{DNSRecords: []Record{ @@ -108,11 +148,16 @@ func TestClient_UpdateDomain(t *testing.T) { } func TestClient_UpdateDomain_error(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/example.com/update", - servermock.ResponseFromFixture("update-domain-error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client := setupTest(t, "/domains/example.com/update", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + rw.WriteHeader(http.StatusUnauthorized) + + writeFixture(rw, "update-domain.json") + }) msg := &DomainInfo{DNSRecords: []Record{ {Type: "MX", Name: "example.com", Value: "fallback.axc.eu", Priority: 20, TTL: 3600}, @@ -129,6 +174,6 @@ func TestClient_UpdateDomain_error(t *testing.T) { {Type: "A", Name: "redirect.example.com", Value: "localhost", Priority: 10, TTL: 14400}, }} - _, err := client.UpdateDomain(t.Context(), "example.com", msg) + _, err := client.UpdateDomain(context.Background(), "example.com", msg) require.ErrorAs(t, err, &ErrorMessage{}) } diff --git a/providers/dns/versio/internal/fixtures/update-domain-request.json b/providers/dns/versio/internal/fixtures/update-domain-request.json deleted file mode 100644 index f351678fc..000000000 --- a/providers/dns/versio/internal/fixtures/update-domain-request.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "dns_records": [ - { - "type": "MX", - "name": "example.com", - "value": "fallback.axc.eu", - "prio": 20, - "ttl": 3600 - }, - { - "type": "TXT", - "name": "example.com", - "value": "\"v=spf1 a mx ip4:127.0.0.1 a:spf.spamexperts.axc.nl ~all\"", - "ttl": 3600 - }, - { - "type": "A", - "name": "example.com", - "value": "185.13.227.159", - "ttl": 14400 - }, - { - "type": "A", - "name": "ftp.example.com", - "value": "185.13.227.159", - "ttl": 14400 - }, - { - "type": "A", - "name": "localhost.example.com", - "value": "185.13.227.159", - "ttl": 14400 - }, - { - "type": "A", - "name": "pop.example.com", - "value": "185.13.227.159", - "ttl": 14400 - }, - { - "type": "A", - "name": "smtp.example.com", - "value": "185.13.227.159", - "ttl": 14400 - }, - { - "type": "A", - "name": "www.example.com", - "value": "185.13.227.159", - "ttl": 14400 - }, - { - "type": "A", - "name": "dev.example.com", - "value": "185.13.227.159", - "ttl": 14400 - }, - { - "type": "A", - "name": "_domainkey.domain.com.example.com", - "value": "185.13.227.159", - "ttl": 14400 - }, - { - "type": "MX", - "name": "example.com", - "value": "spamfilter2.axc.eu", - "ttl": 3600 - }, - { - "type": "A", - "name": "redirect.example.com", - "value": "localhost", - "prio": 10, - "ttl": 14400 - } - ] -} diff --git a/providers/dns/versio/versio.go b/providers/dns/versio/versio.go index 05a7263c4..b1310f0bc 100644 --- a/providers/dns/versio/versio.go +++ b/providers/dns/versio/versio.go @@ -10,10 +10,8 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/versio/internal" ) @@ -32,8 +30,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL *url.URL @@ -56,7 +52,7 @@ func NewDefaultConfig() *Config { return &Config{ BaseURL: baseURL, TTL: env.GetOrDefaultInt(EnvTTL, 300), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 60*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second), SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), HTTPClient: &http.Client{ @@ -92,11 +88,9 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("versio: the configuration of the DNS provider is nil") } - if config.Username == "" { return nil, errors.New("versio: the versio username is missing") } - if config.Password == "" { return nil, errors.New("versio: the versio password is missing") } @@ -111,8 +105,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{config: config, client: client}, nil } @@ -160,7 +152,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("versio: %w", err) } - return nil } @@ -188,7 +179,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { // loop through the existing entries and remove the specific record msg := &internal.DomainInfo{} - for _, e := range domains.DomainInfo.DNSRecords { if e.Name != info.EffectiveFQDN { msg.DNSRecords = append(msg.DNSRecords, e) @@ -199,6 +189,5 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("versio: %w", err) } - return nil } diff --git a/providers/dns/versio/versio.toml b/providers/dns/versio/versio.toml index 733947095..6f57bc037 100644 --- a/providers/dns/versio/versio.toml +++ b/providers/dns/versio/versio.toml @@ -7,7 +7,7 @@ Since = "v2.7.0" Example = ''' VERSIO_USERNAME= \ VERSIO_PASSWORD= \ -lego --dns versio -d '*.example.com' -d example.com run +lego --email you@example.com --dns versio --domains my.example.org run ''' Additional = ''' @@ -20,11 +20,11 @@ To test with the sandbox environment set ```VERSIO_ENDPOINT=https://www.versio.n VERSIO_PASSWORD = "Basic authentication password" [Configuration.Additional] VERSIO_ENDPOINT = "The endpoint URL of the API Server" - VERSIO_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 5)" - VERSIO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - VERSIO_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 60)" - VERSIO_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - VERSIO_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + VERSIO_POLLING_INTERVAL = "Time between DNS propagation check" + VERSIO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + VERSIO_HTTP_TIMEOUT = "API request timeout" + VERSIO_SEQUENCE_INTERVAL = "Time between sequential requests, default 60s" + VERSIO_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://www.versio.nl/RESTapidoc/" diff --git a/providers/dns/versio/versio_mock_test.go b/providers/dns/versio/versio_mock_test.go new file mode 100644 index 000000000..07dc74e83 --- /dev/null +++ b/providers/dns/versio/versio_mock_test.go @@ -0,0 +1,13 @@ +package versio + +const tokenResponseMock = ` +{ + "access_token":"699dd4ff-e381-46b8-8bf8-5de49dd56c1f", + "token_type":"bearer", + "expires_in":3600 +} +` + +const tokenFailToFindZoneMock = `{"error":{"code":401,"message":"ObjectDoesNotExist|Domain not found"}}` + +const tokenFailToCreateTXTMock = `{"error":{"code":400,"message":"ProcessError|DNS record invalid type _acme-challenge.example.eu. TST"}}` diff --git a/providers/dns/versio/versio_test.go b/providers/dns/versio/versio_test.go index 563e70d05..09040ab4c 100644 --- a/providers/dns/versio/versio_test.go +++ b/providers/dns/versio/versio_test.go @@ -1,12 +1,14 @@ package versio import ( + "fmt" + "io" "net/http" "net/http/httptest" "testing" + "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -54,7 +56,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -124,37 +125,21 @@ func TestNewDNSProviderConfig(t *testing.T) { func TestDNSProvider_Present(t *testing.T) { testCases := []struct { desc string - builder *servermock.Builder[*DNSProvider] + handler http.Handler expectedError string }{ { - desc: "Success", - builder: mockBuilder(). - Route("GET /domains/example.com", - servermock.ResponseFromFixture("token.json"), - servermock.CheckQueryParameter().Strict(). - With("show_dns_records", "true")). - Route("POST /domains/example.com/update", - servermock.ResponseFromFixture("token.json")), + desc: "Success", + handler: muxSuccess(), }, { - desc: "FailToFindZone", - builder: mockBuilder(). - Route("GET /domains/example.com", - servermock.ResponseFromFixture("error_failToFindZone.json"). - WithStatusCode(http.StatusUnauthorized)), + desc: "FailToFindZone", + handler: muxFailToFindZone(), expectedError: `versio: [status code: 401] 401: ObjectDoesNotExist|Domain not found`, }, { - desc: "FailToCreateTXT", - builder: mockBuilder(). - Route("GET /domains/example.com", - servermock.ResponseFromFixture("token.json"), - servermock.CheckQueryParameter().Strict(). - With("show_dns_records", "true")). - Route("POST /domains/example.com/update", - servermock.ResponseFromFixture("error_failToCreateTXT.json"). - WithStatusCode(http.StatusBadRequest)), + desc: "FailToCreateTXT", + handler: muxFailToCreateTXT(), expectedError: `versio: [status code: 400] 400: ProcessError|DNS record invalid type _acme-challenge.example.eu. TST`, }, } @@ -162,12 +147,19 @@ func TestDNSProvider_Present(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() - provider := test.builder.Build(t) + baseURL := setupTest(t, test.handler) - err := provider.Present(testDomain, "token", "keyAuth") + envTest.Apply(map[string]string{ + EnvUsername: "me@example.com", + EnvPassword: "secret", + EnvEndpoint: baseURL, + }) + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(testDomain, "token", "keyAuth") if test.expectedError == "" { require.NoError(t, err) } else { @@ -180,25 +172,16 @@ func TestDNSProvider_Present(t *testing.T) { func TestDNSProvider_CleanUp(t *testing.T) { testCases := []struct { desc string - builder *servermock.Builder[*DNSProvider] + handler http.Handler expectedError string }{ { - desc: "Success", - builder: mockBuilder(). - Route("GET /domains/example.com", - servermock.ResponseFromFixture("token.json"), - servermock.CheckQueryParameter().Strict(). - With("show_dns_records", "true")). - Route("POST /domains/example.com/update", - servermock.ResponseFromFixture("token.json")), + desc: "Success", + handler: muxSuccess(), }, { - desc: "FailToFindZone", - builder: mockBuilder(). - Route("GET /domains/example.com", - servermock.ResponseFromFixture("error_failToFindZone.json"). - WithStatusCode(http.StatusUnauthorized)), + desc: "FailToFindZone", + handler: muxFailToFindZone(), expectedError: `versio: [status code: 401] 401: ObjectDoesNotExist|Domain not found`, }, } @@ -206,12 +189,20 @@ func TestDNSProvider_CleanUp(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() - provider := test.builder.Build(t) + baseURL := setupTest(t, test.handler) - err := provider.CleanUp(testDomain, "token", "keyAuth") + envTest.Apply(map[string]string{ + EnvUsername: "me@example.com", + EnvPassword: "secret", + EnvEndpoint: baseURL, + }) + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp(testDomain, "token", "keyAuth") if test.expectedError == "" { require.NoError(t, err) } else { @@ -221,13 +212,91 @@ func TestDNSProvider_CleanUp(t *testing.T) { } } +func muxSuccess() *http.ServeMux { + mux := http.NewServeMux() + + mux.HandleFunc("/domains/example.com", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet && r.URL.Query().Get("show_dns_records") == "true" { + fmt.Fprint(w, tokenResponseMock) + return + } + w.WriteHeader(http.StatusBadRequest) + }) + + mux.HandleFunc("/domains/example.com/update", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + fmt.Fprint(w, tokenResponseMock) + return + } + w.WriteHeader(http.StatusBadRequest) + }) + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + log.Printf("unexpected request: %+v\n\n", r) + data, _ := io.ReadAll(r.Body) + defer func() { _ = r.Body.Close() }() + log.Println(string(data)) + http.NotFound(w, r) + }) + + return mux +} + +func muxFailToFindZone() *http.ServeMux { + mux := http.NewServeMux() + + mux.HandleFunc("/domains/example.com", func(w http.ResponseWriter, _ *http.Request) { + http.Error(w, tokenFailToFindZoneMock, http.StatusUnauthorized) + }) + + return mux +} + +func muxFailToCreateTXT() *http.ServeMux { + mux := http.NewServeMux() + + mux.HandleFunc("/domains/example.com", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet && r.URL.Query().Get("show_dns_records") == "true" { + fmt.Fprint(w, tokenResponseMock) + return + } + w.WriteHeader(http.StatusBadRequest) + }) + + mux.HandleFunc("/domains/example.com/update", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + http.Error(w, tokenFailToCreateTXTMock, http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusBadRequest) + }) + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + log.Printf("unexpected request: %+v\n\n", r) + data, _ := io.ReadAll(r.Body) + defer func() { _ = r.Body.Close() }() + log.Println(string(data)) + http.NotFound(w, r) + }) + + return mux +} + +func setupTest(t *testing.T, handler http.Handler) string { + t.Helper() + + server := httptest.NewServer(handler) + t.Cleanup(server.Close) + + return server.URL +} + func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -241,29 +310,9 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) { - envTest.Apply(map[string]string{ - EnvUsername: "me@example.com", - EnvPassword: "secret", - EnvEndpoint: server.URL, - }) - - provider, err := NewDNSProvider() - if err != nil { - return nil, err - } - - provider.client.HTTPClient = server.Client() - - return provider, nil - }) -} diff --git a/providers/dns/vinyldns/mock_test.go b/providers/dns/vinyldns/mock_test.go new file mode 100644 index 000000000..54fd8e214 --- /dev/null +++ b/providers/dns/vinyldns/mock_test.go @@ -0,0 +1,114 @@ +package vinyldns + +import ( + "fmt" + "net/http" + "net/http/httptest" + "os" + "sync" + "testing" + + "github.com/stretchr/testify/require" +) + +func setupTest(t *testing.T) (*http.ServeMux, *DNSProvider) { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + config := NewDefaultConfig() + config.AccessKey = "foo" + config.SecretKey = "bar" + config.Host = server.URL + + p, err := NewDNSProviderConfig(config) + require.NoError(t, err) + + return mux, p +} + +type mockRouter struct { + debug bool + + mu sync.Mutex + routes map[string]map[string]http.HandlerFunc +} + +func newMockRouter() *mockRouter { + routes := map[string]map[string]http.HandlerFunc{ + http.MethodGet: {}, + http.MethodPost: {}, + http.MethodPut: {}, + http.MethodDelete: {}, + } + + return &mockRouter{ + routes: routes, + } +} + +func (h *mockRouter) Debug() *mockRouter { + h.debug = true + + return h +} + +func (h *mockRouter) Get(path string, statusCode int, filename string) *mockRouter { + h.add(http.MethodGet, path, statusCode, filename) + return h +} + +func (h *mockRouter) Post(path string, statusCode int, filename string) *mockRouter { + h.add(http.MethodPost, path, statusCode, filename) + return h +} + +func (h *mockRouter) Put(path string, statusCode int, filename string) *mockRouter { + h.add(http.MethodPut, path, statusCode, filename) + return h +} + +func (h *mockRouter) Delete(path string, statusCode int, filename string) *mockRouter { + h.add(http.MethodDelete, path, statusCode, filename) + return h +} + +func (h *mockRouter) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + h.mu.Lock() + defer h.mu.Unlock() + + if h.debug { + fmt.Println(req) + } + + rt := h.routes[req.Method] + if rt == nil { + http.NotFound(rw, req) + return + } + + hdl := rt[req.URL.Path] + if hdl == nil { + http.NotFound(rw, req) + return + } + + hdl(rw, req) +} + +func (h *mockRouter) add(method, path string, statusCode int, filename string) { + h.routes[method][path] = func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(statusCode) + + data, err := os.ReadFile(fmt.Sprintf("./fixtures/%s.json", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + rw.Header().Set("Content-Type", "application/json") + _, _ = rw.Write(data) + } +} diff --git a/providers/dns/vinyldns/vinyldns.go b/providers/dns/vinyldns/vinyldns.go index 65a024513..dca58fb9f 100644 --- a/providers/dns/vinyldns/vinyldns.go +++ b/providers/dns/vinyldns/vinyldns.go @@ -2,18 +2,12 @@ package vinyldns import ( - "context" "errors" "fmt" - "net/http" - "strconv" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" "github.com/vinyldns/go-vinyldns/vinyldns" ) @@ -21,30 +15,23 @@ import ( const ( envNamespace = "VINYLDNS_" - EnvAccessKey = envNamespace + "ACCESS_KEY" - EnvSecretKey = envNamespace + "SECRET_KEY" - EnvHost = envNamespace + "HOST" - EnvQuoteValue = envNamespace + "QUOTE_VALUE" + EnvAccessKey = envNamespace + "ACCESS_KEY" + EnvSecretKey = envNamespace + "SECRET_KEY" + EnvHost = envNamespace + "HOST" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { - AccessKey string - SecretKey string - Host string - QuoteValue bool - + AccessKey string + SecretKey string + Host string TTL int PropagationTimeout time.Duration PollingInterval time.Duration - HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. @@ -53,9 +40,6 @@ func NewDefaultConfig() *Config { TTL: env.GetOrDefaultInt(EnvTTL, 30), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 4*time.Second), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, } } @@ -78,7 +62,6 @@ func NewDNSProvider() (*DNSProvider, error) { config.AccessKey = values[EnvAccessKey] config.SecretKey = values[EnvSecretKey] config.Host = values[EnvHost] - config.QuoteValue = env.GetOrDefaultBool(EnvQuoteValue, false) return NewDNSProviderConfig(config) } @@ -101,25 +84,16 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { AccessKey: config.AccessKey, SecretKey: config.SecretKey, Host: config.Host, - UserAgent: useragent.Get(), + UserAgent: "go-acme/lego", }) - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } else { - // For compatibility, it should be removed in v5. - client.HTTPClient.Timeout = 30 * time.Second - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + client.HTTPClient.Timeout = 30 * time.Second return &DNSProvider{client: client, config: config}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) existingRecord, err := d.getRecordSet(info.EffectiveFQDN) @@ -127,12 +101,10 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("vinyldns: %w", err) } - value := d.formatValue(info.Value) - - record := vinyldns.Record{Text: value} + record := vinyldns.Record{Text: info.Value} if existingRecord == nil || existingRecord.ID == "" { - err = d.createRecordSet(ctx, info.EffectiveFQDN, []vinyldns.Record{record}) + err = d.createRecordSet(info.EffectiveFQDN, []vinyldns.Record{record}) if err != nil { return fmt.Errorf("vinyldns: %w", err) } @@ -141,7 +113,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { } for _, i := range existingRecord.Records { - if i.Text == value { + if i.Text == info.Value { return nil } } @@ -149,7 +121,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { records := existingRecord.Records records = append(records, record) - err = d.updateRecordSet(ctx, existingRecord, records) + err = d.updateRecordSet(existingRecord, records) if err != nil { return fmt.Errorf("vinyldns: %w", err) } @@ -159,8 +131,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - ctx := context.Background() - info := dns01.GetChallengeInfo(domain, keyAuth) existingRecord, err := d.getRecordSet(info.EffectiveFQDN) @@ -172,18 +142,15 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return nil } - value := d.formatValue(info.Value) - var records []vinyldns.Record - for _, i := range existingRecord.Records { - if i.Text != value { + if i.Text != info.Value { records = append(records, i) } } if len(records) == 0 { - err = d.deleteRecordSet(ctx, existingRecord) + err = d.deleteRecordSet(existingRecord) if err != nil { return fmt.Errorf("vinyldns: %w", err) } @@ -191,7 +158,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return nil } - err = d.updateRecordSet(ctx, existingRecord, records) + err = d.updateRecordSet(existingRecord, records) if err != nil { return fmt.Errorf("vinyldns: %w", err) } @@ -204,11 +171,3 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } - -func (d *DNSProvider) formatValue(v string) string { - if d.config.QuoteValue { - return strconv.Quote(v) - } - - return v -} diff --git a/providers/dns/vinyldns/vinyldns.toml b/providers/dns/vinyldns/vinyldns.toml index d6dd5810e..93062619c 100644 --- a/providers/dns/vinyldns/vinyldns.toml +++ b/providers/dns/vinyldns/vinyldns.toml @@ -8,7 +8,7 @@ Example = ''' VINYLDNS_ACCESS_KEY=xxxxxx \ VINYLDNS_SECRET_KEY=yyyyy \ VINYLDNS_HOST=https://api.vinyldns.example.org:9443 \ -lego --dns vinyldns -d '*.example.com' -d example.com run +lego --email you@example.com --dns vinyldns --domains my.example.org run ''' Additional = ''' @@ -22,11 +22,9 @@ Users are required to have DELETE ACL level or zone admin permissions on the Vin VINYLDNS_SECRET_KEY = "The VinylDNS API Secret key" VINYLDNS_HOST = "The VinylDNS API URL" [Configuration.Additional] - VINYLDNS_QUOTE_VALUE = "Adds quotes around the TXT record value (Default: false)" - VINYLDNS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 4)" - VINYLDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - VINYLDNS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 30)" - VINYLDNS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + VINYLDNS_POLLING_INTERVAL = "Time between DNS propagation check" + VINYLDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + VINYLDNS_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://www.vinyldns.io/api/" diff --git a/providers/dns/vinyldns/vinyldns_test.go b/providers/dns/vinyldns/vinyldns_test.go index 7dfe2c13f..8bfb192c8 100644 --- a/providers/dns/vinyldns/vinyldns_test.go +++ b/providers/dns/vinyldns/vinyldns_test.go @@ -2,12 +2,10 @@ package vinyldns import ( "net/http" - "net/http/httptest" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/require" ) @@ -78,7 +76,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -157,87 +154,63 @@ func TestNewDNSProviderConfig(t *testing.T) { } } -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.AccessKey = "foo" - config.SecretKey = "bar" - config.Host = server.URL - config.HTTPClient = server.Client() - - return NewDNSProviderConfig(config) - }) -} - func TestDNSProvider_Present(t *testing.T) { testCases := []struct { desc string keyAuth string - builder *servermock.Builder[*DNSProvider] + handler http.Handler }{ { desc: "new record", keyAuth: "123456d==", - builder: mockBuilder(). - Route("GET /zones/name/"+targetRootDomain+".", - servermock.ResponseFromFixture("zoneByName.json")). - Route("GET /zones/"+zoneID+"/recordsets", - servermock.ResponseFromFixture("recordSetsListAll-empty.json")). - Route("POST /zones/"+zoneID+"/recordsets", - servermock.ResponseFromFixture("recordSetUpdate-create.json"). - WithStatusCode(http.StatusAccepted)). - Route("GET /zones/"+zoneID+"/recordsets/"+newRecordSetID+"/changes/"+newCreateChangeID, - servermock.ResponseFromFixture("recordSetChange-create.json")), + handler: newMockRouter(). + Get("/zones/name/"+targetRootDomain+".", http.StatusOK, "zoneByName"). + Get("/zones/"+zoneID+"/recordsets", http.StatusOK, "recordSetsListAll-empty"). + Post("/zones/"+zoneID+"/recordsets", http.StatusAccepted, "recordSetUpdate-create"). + Get("/zones/"+zoneID+"/recordsets/"+newRecordSetID+"/changes/"+newCreateChangeID, http.StatusOK, "recordSetChange-create"), }, { desc: "existing record", keyAuth: "123456d==", - builder: mockBuilder(). - Route("GET /zones/name/"+targetRootDomain+".", - servermock.ResponseFromFixture("zoneByName.json")). - Route("GET /zones/"+zoneID+"/recordsets", - servermock.ResponseFromFixture("recordSetsListAll.json")), + handler: newMockRouter(). + Get("/zones/name/"+targetRootDomain+".", http.StatusOK, "zoneByName"). + Get("/zones/"+zoneID+"/recordsets", http.StatusOK, "recordSetsListAll"), }, { desc: "duplicate key", keyAuth: "abc123!!", - builder: mockBuilder(). - Route("GET /zones/name/"+targetRootDomain+".", - servermock.ResponseFromFixture("zoneByName.json")). - Route("GET /zones/"+zoneID+"/recordsets", - servermock.ResponseFromFixture("recordSetsListAll.json")). - Route("PUT /zones/"+zoneID+"/recordsets/"+recordID, - servermock.ResponseFromFixture("recordSetUpdate-create.json"). - WithStatusCode(http.StatusAccepted)). - Route("GET /zones/"+zoneID+"/recordsets/"+newRecordSetID+"/changes/"+newCreateChangeID, - servermock.ResponseFromFixture("recordSetChange-create.json")), + handler: newMockRouter(). + Get("/zones/name/"+targetRootDomain+".", http.StatusOK, "zoneByName"). + Get("/zones/"+zoneID+"/recordsets", http.StatusOK, "recordSetsListAll"). + Put("/zones/"+zoneID+"/recordsets/"+recordID, http.StatusAccepted, "recordSetUpdate-create"). + Get("/zones/"+zoneID+"/recordsets/"+newRecordSetID+"/changes/"+newCreateChangeID, http.StatusOK, "recordSetChange-create"), }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - provider := test.builder.Build(t) + t.Parallel() - err := provider.Present(targetDomain, "token"+test.keyAuth, test.keyAuth) + mux, p := setupTest(t) + mux.Handle("/", test.handler) + + err := p.Present(targetDomain, "token"+test.keyAuth, test.keyAuth) require.NoError(t, err) }) } } func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("GET /zones/name/"+targetRootDomain+".", - servermock.ResponseFromFixture("zoneByName.json")). - Route("GET /zones/"+zoneID+"/recordsets", - servermock.ResponseFromFixture("recordSetsListAll.json")). - Route("DELETE /zones/"+zoneID+"/recordsets/"+recordID, - servermock.ResponseFromFixture("recordSetDelete.json"). - WithStatusCode(http.StatusAccepted)). - Route("GET /zones/"+zoneID+"/recordsets/"+newRecordSetID+"/changes/"+newCreateChangeID, - servermock.ResponseFromFixture("recordSetChange-delete.json")). - Build(t) + mux, p := setupTest(t) - err := provider.CleanUp(targetDomain, "123456d==", "123456d==") + mux.Handle("/", newMockRouter(). + Get("/zones/name/"+targetRootDomain+".", http.StatusOK, "zoneByName"). + Get("/zones/"+zoneID+"/recordsets", http.StatusOK, "recordSetsListAll"). + Delete("/zones/"+zoneID+"/recordsets/"+recordID, http.StatusAccepted, "recordSetDelete"). + Get("/zones/"+zoneID+"/recordsets/"+newRecordSetID+"/changes/"+newCreateChangeID, http.StatusOK, "recordSetChange-delete"), + ) + + err := p.CleanUp(targetDomain, "123456d==", "123456d==") require.NoError(t, err) } @@ -247,7 +220,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -261,7 +233,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/vinyldns/wrapper.go b/providers/dns/vinyldns/wrapper.go index e7b59a82b..f17b3de31 100644 --- a/providers/dns/vinyldns/wrapper.go +++ b/providers/dns/vinyldns/wrapper.go @@ -1,10 +1,8 @@ package vinyldns import ( - "context" "fmt" - "github.com/cenkalti/backoff/v5" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/wait" "github.com/vinyldns/go-vinyldns/vinyldns" @@ -27,7 +25,6 @@ func (d *DNSProvider) getRecordSet(fqdn string) (*vinyldns.RecordSet, error) { } var recordSets []vinyldns.RecordSet - for _, i := range allRecordSets { if i.Type == "TXT" { recordSets = append(recordSets, i) @@ -44,7 +41,7 @@ func (d *DNSProvider) getRecordSet(fqdn string) (*vinyldns.RecordSet, error) { } } -func (d *DNSProvider) createRecordSet(ctx context.Context, fqdn string, records []vinyldns.Record) error { +func (d *DNSProvider) createRecordSet(fqdn string, records []vinyldns.Record) error { zoneName, hostName, err := splitDomain(fqdn) if err != nil { return err @@ -68,10 +65,10 @@ func (d *DNSProvider) createRecordSet(ctx context.Context, fqdn string, records return err } - return d.waitForChanges(ctx, "CreateRS", resp) + return d.waitForChanges("CreateRS", resp) } -func (d *DNSProvider) updateRecordSet(ctx context.Context, recordSet *vinyldns.RecordSet, newRecords []vinyldns.Record) error { +func (d *DNSProvider) updateRecordSet(recordSet *vinyldns.RecordSet, newRecords []vinyldns.Record) error { operation := "delete" if len(recordSet.Records) < len(newRecords) { operation = "add" @@ -85,35 +82,33 @@ func (d *DNSProvider) updateRecordSet(ctx context.Context, recordSet *vinyldns.R return err } - return d.waitForChanges(ctx, "UpdateRS - "+operation, resp) + return d.waitForChanges("UpdateRS - "+operation, resp) } -func (d *DNSProvider) deleteRecordSet(ctx context.Context, existingRecord *vinyldns.RecordSet) error { +func (d *DNSProvider) deleteRecordSet(existingRecord *vinyldns.RecordSet) error { resp, err := d.client.RecordSetDelete(existingRecord.ZoneID, existingRecord.ID) if err != nil { return err } - return d.waitForChanges(ctx, "DeleteRS", resp) + return d.waitForChanges("DeleteRS", resp) } -func (d *DNSProvider) waitForChanges(ctx context.Context, operation string, resp *vinyldns.RecordSetUpdateResponse) error { - return wait.Retry(ctx, - func() error { +func (d *DNSProvider) waitForChanges(operation string, resp *vinyldns.RecordSetUpdateResponse) error { + return wait.For("vinyldns", d.config.PropagationTimeout, d.config.PollingInterval, + func() (bool, error) { change, err := d.client.RecordSetChange(resp.Zone.ID, resp.RecordSet.ID, resp.ChangeID) if err != nil { - return fmt.Errorf("failed to query change status: %w", err) + return false, fmt.Errorf("failed to query change status: %w", err) } - if change.Status != "Complete" { - return fmt.Errorf("waiting operation: %s, zoneID: %s, recordsetID: %s, changeID: %s", - operation, resp.Zone.ID, resp.RecordSet.ID, resp.ChangeID) + if change.Status == "Complete" { + return true, nil } - return nil + return false, fmt.Errorf("waiting operation: %s, zoneID: %s, recordsetID: %s, changeID: %s", + operation, resp.Zone.ID, resp.RecordSet.ID, resp.ChangeID) }, - backoff.WithBackOff(backoff.NewConstantBackOff(d.config.PollingInterval)), - backoff.WithMaxElapsedTime(d.config.PropagationTimeout), ) } diff --git a/providers/dns/virtualname/virtualname.go b/providers/dns/virtualname/virtualname.go deleted file mode 100644 index 34637d280..000000000 --- a/providers/dns/virtualname/virtualname.go +++ /dev/null @@ -1,103 +0,0 @@ -// Package virtualname implements a DNS provider for solving the DNS-01 challenge using Virtualname DNS. -package virtualname - -import ( - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/tecnocratica" -) - -// Environment variables names. -const ( - envNamespace = "VIRTUALNAME_" - - EnvToken = envNamespace + "TOKEN" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -const defaultBaseURL = "https://api.virtualname.net/v1" - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config = tecnocratica.Config - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - prv challenge.ProviderTimeout -} - -// NewDNSProvider returns a DNSProvider instance configured for Virtualname. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvToken) - if err != nil { - return nil, fmt.Errorf("virtualname: %w", err) - } - - config := NewDefaultConfig() - config.Token = values[EnvToken] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Virtualname. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("virtualname: the configuration of the DNS provider is nil") - } - - provider, err := tecnocratica.NewDNSProviderConfig(config, defaultBaseURL) - if err != nil { - return nil, fmt.Errorf("virtualname: %w", err) - } - - return &DNSProvider{prv: provider}, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - err := d.prv.Present(domain, token, keyAuth) - if err != nil { - return fmt.Errorf("virtualname: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - err := d.prv.CleanUp(domain, token, keyAuth) - if err != nil { - return fmt.Errorf("virtualname: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.prv.Timeout() -} diff --git a/providers/dns/virtualname/virtualname.toml b/providers/dns/virtualname/virtualname.toml deleted file mode 100644 index 881f09797..000000000 --- a/providers/dns/virtualname/virtualname.toml +++ /dev/null @@ -1,22 +0,0 @@ -Name = "Virtualname" -Description = '''''' -URL = "https://www.virtualname.es/" -Code = "virtualname" -Since = "v4.30.0" - -Example = ''' -VIRTUALNAME_TOKEN=xxxxxx \ -lego --dns virtualname -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - VIRTUALNAME_TOKEN = "API token" - [Configuration.Additional] - VIRTUALNAME_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" - VIRTUALNAME_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 300)" - VIRTUALNAME_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - VIRTUALNAME_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://developers.virtualname.net/#dns" diff --git a/providers/dns/virtualname/virtualname_test.go b/providers/dns/virtualname/virtualname_test.go deleted file mode 100644 index da5867e86..000000000 --- a/providers/dns/virtualname/virtualname_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package virtualname - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvToken: "secret", - }, - }, - { - desc: "missing credentials: token", - envVars: map[string]string{ - EnvToken: "", - }, - expected: "virtualname: some credentials information are missing: VIRTUALNAME_TOKEN", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.prv) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - token string - expected string - }{ - { - desc: "success", - token: "secret", - }, - { - desc: "missing token", - expected: "virtualname: missing credentials", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.Token = test.token - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.prv) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/vkcloud/internal/client.go b/providers/dns/vkcloud/internal/client.go index 2b03518db..5ced88d2d 100644 --- a/providers/dns/vkcloud/internal/client.go +++ b/providers/dns/vkcloud/internal/client.go @@ -46,7 +46,6 @@ func (c *Client) ListZones() ([]DNSZone, error) { endpoint := c.baseURL.JoinPath("/") var zones []DNSZone - opts := &gophercloud.RequestOpts{JSONResponse: &zones} err := c.request(http.MethodGet, endpoint, opts) @@ -61,7 +60,6 @@ func (c *Client) ListTXTRecords(zoneUUID string) ([]DNSTXTRecord, error) { endpoint := c.baseURL.JoinPath(zoneUUID, "txt", "/") var records []DNSTXTRecord - opts := &gophercloud.RequestOpts{JSONResponse: &records} err := c.request(http.MethodGet, endpoint, opts) diff --git a/providers/dns/vkcloud/vkcloud.go b/providers/dns/vkcloud/vkcloud.go index ffacdbe52..d27feca81 100644 --- a/providers/dns/vkcloud/vkcloud.go +++ b/providers/dns/vkcloud/vkcloud.go @@ -6,13 +6,19 @@ import ( "fmt" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/vkcloud/internal" "github.com/gophercloud/gophercloud" ) +const ( + defaultIdentityEndpoint = "https://infra.mail.ru/identity/v3/" + defaultDNSEndpoint = "https://mcs.mail.ru/public-dns/v2/dns" +) + +const defaultDomainName = "users" + // Environment variables names. const ( envNamespace = "VK_CLOUD_" @@ -31,15 +37,6 @@ const ( EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) -const ( - defaultIdentityEndpoint = "https://infra.mail.ru/identity/v3/" - defaultDNSEndpoint = "https://mcs.mail.ru/public-dns/v2/dns" -) - -const defaultDomainName = "users" - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { ProjectID string @@ -119,7 +116,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { } // Present creates a TXT record to fulfill the dns-01 challenge. -func (d *DNSProvider) Present(domain, _, keyAuth string) error { +func (r *DNSProvider) Present(domain, _, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) @@ -129,13 +126,12 @@ func (d *DNSProvider) Present(domain, _, keyAuth string) error { authZone = dns01.UnFqdn(authZone) - zones, err := d.client.ListZones() + zones, err := r.client.ListZones() if err != nil { return fmt.Errorf("vkcloud: unable to fetch dns zones: %w", err) } var zoneUUID string - for _, zone := range zones { if zone.Zone == authZone { zoneUUID = zone.UUID @@ -151,7 +147,7 @@ func (d *DNSProvider) Present(domain, _, keyAuth string) error { return fmt.Errorf("vkcloud: %w", err) } - err = d.upsertTXTRecord(zoneUUID, subDomain, info.Value) + err = r.upsertTXTRecord(zoneUUID, subDomain, info.Value) if err != nil { return fmt.Errorf("vkcloud: %w", err) } @@ -160,7 +156,7 @@ func (d *DNSProvider) Present(domain, _, keyAuth string) error { } // CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { +func (r *DNSProvider) CleanUp(domain, _, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) @@ -170,7 +166,7 @@ func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { authZone = dns01.UnFqdn(authZone) - zones, err := d.client.ListZones() + zones, err := r.client.ListZones() if err != nil { return fmt.Errorf("vkcloud: unable to fetch dns zones: %w", err) } @@ -192,7 +188,7 @@ func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { return fmt.Errorf("vkcloud: %w", err) } - err = d.removeTXTRecord(zoneUUID, subDomain, info.Value) + err = r.removeTXTRecord(zoneUUID, subDomain, info.Value) if err != nil { return fmt.Errorf("vkcloud: %w", err) } @@ -202,12 +198,12 @@ func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval +func (r *DNSProvider) Timeout() (timeout, interval time.Duration) { + return r.config.PropagationTimeout, r.config.PollingInterval } -func (d *DNSProvider) upsertTXTRecord(zoneUUID, name, value string) error { - records, err := d.client.ListTXTRecords(zoneUUID) +func (r *DNSProvider) upsertTXTRecord(zoneUUID, name, value string) error { + records, err := r.client.ListTXTRecords(zoneUUID) if err != nil { return err } @@ -219,15 +215,15 @@ func (d *DNSProvider) upsertTXTRecord(zoneUUID, name, value string) error { } } - return d.client.CreateTXTRecord(zoneUUID, &internal.DNSTXTRecord{ + return r.client.CreateTXTRecord(zoneUUID, &internal.DNSTXTRecord{ Name: name, Content: value, - TTL: d.config.TTL, + TTL: r.config.TTL, }) } -func (d *DNSProvider) removeTXTRecord(zoneUUID, name, value string) error { - records, err := d.client.ListTXTRecords(zoneUUID) +func (r *DNSProvider) removeTXTRecord(zoneUUID, name, value string) error { + records, err := r.client.ListTXTRecords(zoneUUID) if err != nil { return err } @@ -235,7 +231,7 @@ func (d *DNSProvider) removeTXTRecord(zoneUUID, name, value string) error { name = dns01.UnFqdn(name) for _, record := range records { if record.Name == name && record.Content == value { - return d.client.DeleteTXTRecord(zoneUUID, record.UUID) + return r.client.DeleteTXTRecord(zoneUUID, record.UUID) } } diff --git a/providers/dns/vkcloud/vkcloud.toml b/providers/dns/vkcloud/vkcloud.toml index 04f57fea3..20beeefd6 100644 --- a/providers/dns/vkcloud/vkcloud.toml +++ b/providers/dns/vkcloud/vkcloud.toml @@ -8,7 +8,7 @@ Example = ''' VK_CLOUD_PROJECT_ID="" \ VK_CLOUD_USERNAME="" \ VK_CLOUD_PASSWORD="" \ -lego --dns vkcloud -d '*.example.com' -d example.com run +lego --email you@example.com --dns vkcloud --domains "example.org" --domains "*.example.org" run ''' Additional = ''' @@ -33,9 +33,9 @@ You can find all required and additional information on ["Project/Keys" page](ht VK_CLOUD_DNS_ENDPOINT="URL of DNS API. Defaults to https://mcs.mail.ru/public-dns but can be changed for usage with private clouds" VK_CLOUD_IDENTITY_ENDPOINT="URL of OpenStack Auth API, Defaults to https://infra.mail.ru:35357/v3/ but can be changed for usage with private clouds" VK_CLOUD_DOMAIN_NAME="Openstack users domain name. Defaults to `users` but can be changed for usage with private clouds" - VK_CLOUD_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - VK_CLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - VK_CLOUD_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" + VK_CLOUD_POLLING_INTERVAL = "Time between DNS propagation check" + VK_CLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + VK_CLOUD_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://mcs.mail.ru/docs/networks/vnet/networks/publicdns/api" diff --git a/providers/dns/vkcloud/vkcloud_test.go b/providers/dns/vkcloud/vkcloud_test.go index e7883b486..edc32363a 100644 --- a/providers/dns/vkcloud/vkcloud_test.go +++ b/providers/dns/vkcloud/vkcloud_test.go @@ -60,7 +60,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -189,7 +188,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -203,7 +201,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/volcengine/volcengine.go b/providers/dns/volcengine/volcengine.go deleted file mode 100644 index 765d38adb..000000000 --- a/providers/dns/volcengine/volcengine.go +++ /dev/null @@ -1,238 +0,0 @@ -// Package volcengine implements a DNS provider for solving the DNS-01 challenge using Volcano Engine. -package volcengine - -import ( - "context" - "errors" - "fmt" - "net/http" - "sync" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/ptr" - "github.com/volcengine/volc-sdk-golang/base" - volc "github.com/volcengine/volc-sdk-golang/service/dns" -) - -// Environment variables names. -const ( - envNamespace = "VOLC_" - - EnvAccessKey = envNamespace + "ACCESSKEY" - EnvSecretKey = envNamespace + "SECRETKEY" - - EnvRegion = envNamespace + "REGION" - EnvHost = envNamespace + "HOST" - EnvScheme = envNamespace + "SCHEME" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// https://www.volcengine.com/docs/6758/170354 -const defaultTTL = 600 - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - AccessKey string - SecretKey string - - Region string - Host string - Scheme string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPTimeout time.Duration -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - Scheme: env.GetOrDefaultString(EnvScheme, "https"), - Host: env.GetOrDefaultString(EnvHost, "open.volcengineapi.com"), - Region: env.GetOrDefaultString(EnvRegion, volc.DefaultRegion), - - TTL: env.GetOrDefaultInt(EnvTTL, defaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 4*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), - HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, volc.Timeout*time.Second), - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - client *volc.Client - config *Config - - recordIDs map[string]*string - recordIDsMu sync.Mutex -} - -// NewDNSProvider returns a DNSProvider instance configured for Volcano Engine. -// Credentials must be passed in the environment variable: VOLC_ACCESSKEY, VOLC_SECRETKEY. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAccessKey, EnvSecretKey) - if err != nil { - return nil, fmt.Errorf("volcengine: %w", err) - } - - config := NewDefaultConfig() - config.AccessKey = values[EnvAccessKey] - config.SecretKey = values[EnvSecretKey] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for Volcano Engine. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("volcengine: the configuration of the DNS provider is nil") - } - - if config.AccessKey == "" || config.SecretKey == "" { - return nil, errors.New("volcengine: missing credentials") - } - - return &DNSProvider{ - config: config, - client: newClient(config), - recordIDs: make(map[string]*string), - }, nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} - -// Present creates a TXT record to fulfill the dns-01 challenge. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - ctx := context.Background() - - info := dns01.GetChallengeInfo(domain, keyAuth) - - zone, err := d.getZone(ctx, info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("volcengine: get zone ID: %w", err) - } - - subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, ptr.Deref(zone.ZoneName)) - if err != nil { - return fmt.Errorf("volcengine: %w", err) - } - - crr := &volc.CreateRecordRequest{ - Host: ptr.Pointer(subDomain), - TTL: ptr.Pointer(int64(d.config.TTL)), - Type: ptr.Pointer("TXT"), - Value: ptr.Pointer(info.Value), - ZID: zone.ZID, - } - - record, err := d.client.CreateRecord(ctx, crr) - if err != nil { - return fmt.Errorf("volcengine: create record: %w", err) - } - - d.recordIDsMu.Lock() - d.recordIDs[token] = record.RecordID - d.recordIDsMu.Unlock() - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - // gets the record's unique ID - d.recordIDsMu.Lock() - recordID, ok := d.recordIDs[token] - d.recordIDsMu.Unlock() - - if !ok { - return fmt.Errorf("volcengine: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) - } - - drr := &volc.DeleteRecordRequest{RecordID: recordID} - - err := d.client.DeleteRecord(context.Background(), drr) - if err != nil { - return fmt.Errorf("volcengine: delete record: %w", err) - } - - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - - return nil -} - -func (d *DNSProvider) getZone(ctx context.Context, fqdn string) (volc.TopZoneResponse, error) { - for domain := range dns01.UnFqdnDomainsSeq(fqdn) { - lzr := &volc.ListZonesRequest{ - Key: ptr.Pointer(dns01.UnFqdn(domain)), - SearchMode: ptr.Pointer("exact"), - } - - zones, err := d.client.ListZones(ctx, lzr) - if err != nil { - return volc.TopZoneResponse{}, fmt.Errorf("list zones: %w", err) - } - - total := ptr.Deref(zones.Total) - - if total == 0 || len(zones.Zones) == 0 { - continue - } - - if total > 1 { - return volc.TopZoneResponse{}, fmt.Errorf("too many zone for %s", domain) - } - - return zones.Zones[0], nil - } - - return volc.TopZoneResponse{}, fmt.Errorf("zone no found for fqdn: %s", fqdn) -} - -// https://github.com/volcengine/volc-sdk-golang/tree/main/service/dns -// https://github.com/volcengine/volc-sdk-golang/blob/main/example/dns/demo_dns_test.go -func newClient(config *Config) *volc.Client { - // https://github.com/volcengine/volc-sdk-golang/blob/fae992a31d02754e271c322095413d374ea4ea1b/service/dns/config.go#L20-L35 - serviceInfo := &base.ServiceInfo{ - Timeout: config.HTTPTimeout, - Host: config.Host, - Header: http.Header{"Accept": []string{"application/json"}}, - Scheme: config.Scheme, - Credentials: base.Credentials{ - Service: volc.ServiceName, - Region: config.Region, - AccessKeyID: config.AccessKey, - SecretAccessKey: config.SecretKey, - }, - } - - // https://github.com/volcengine/volc-sdk-golang/blob/fae992a31d02754e271c322095413d374ea4ea1b/service/dns/caller.go#L17-L19 - client := base.NewClient(serviceInfo, nil) - - // https://github.com/volcengine/volc-sdk-golang/blob/fae992a31d02754e271c322095413d374ea4ea1b/service/dns/caller.go#L25-L34 - caller := &volc.VolcCaller{Volc: client} - caller.Volc.SetAccessKey(serviceInfo.Credentials.AccessKeyID) - caller.Volc.SetSecretKey(serviceInfo.Credentials.SecretAccessKey) - caller.Volc.SetHost(serviceInfo.Host) - caller.Volc.SetScheme(serviceInfo.Scheme) - caller.Volc.SetTimeout(serviceInfo.Timeout) - - return volc.NewClient(caller) -} diff --git a/providers/dns/volcengine/volcengine.toml b/providers/dns/volcengine/volcengine.toml deleted file mode 100644 index ceedcb18a..000000000 --- a/providers/dns/volcengine/volcengine.toml +++ /dev/null @@ -1,28 +0,0 @@ -Name = "Volcano Engine/火山引擎" -Description = '''''' -URL = "https://www.volcengine.com/" -Code = "volcengine" -Since = "v4.19.0" - -Example = ''' -VOLC_ACCESSKEY=xxx \ -VOLC_SECRETKEY=yyy \ -lego --dns volcengine -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - VOLC_ACCESSKEY = "Access Key ID (AK)" - VOLC_SECRETKEY = "Secret Access Key (SK)" - [Configuration.Additional] - VOLC_REGION = "Region" - VOLC_HOST = "API host" - VOLC_SCHEME = "API scheme" - VOLC_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" - VOLC_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 240)" - VOLC_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)" - VOLC_HTTP_TIMEOUT = "API request timeout in seconds (Default: 15)" - -[Links] - API = "https://www.volcengine.com/docs/6758/155086" - GoClient = "https://github.com/volcengine/volc-sdk-golang" diff --git a/providers/dns/volcengine/volcengine_test.go b/providers/dns/volcengine/volcengine_test.go deleted file mode 100644 index 0f79ed83a..000000000 --- a/providers/dns/volcengine/volcengine_test.go +++ /dev/null @@ -1,151 +0,0 @@ -package volcengine - -import ( - "testing" - "time" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest( - EnvAccessKey, - EnvSecretKey, - EnvRegion, - EnvHost, - EnvScheme). - WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAccessKey: "access", - EnvSecretKey: "secret", - }, - }, - { - desc: "missing access key", - envVars: map[string]string{ - EnvSecretKey: "secret", - }, - expected: "volcengine: some credentials information are missing: VOLC_ACCESSKEY", - }, - { - desc: "missing secret key", - envVars: map[string]string{ - EnvAccessKey: "access", - }, - expected: "volcengine: some credentials information are missing: VOLC_SECRETKEY", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "volcengine: some credentials information are missing: VOLC_ACCESSKEY,VOLC_SECRETKEY", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - expected string - accessKey string - secretKey string - }{ - { - desc: "success", - accessKey: "access", - secretKey: "secret", - }, - { - desc: "missing access key", - secretKey: "secret", - expected: "volcengine: missing credentials", - }, - { - desc: "missing secret key", - accessKey: "access", - expected: "volcengine: missing credentials", - }, - { - desc: "missing credentials", - expected: "volcengine: missing credentials", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.AccessKey = test.accessKey - config.SecretKey = test.secretKey - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - time.Sleep(1 * time.Second) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/vscale/vscale.go b/providers/dns/vscale/vscale.go index a159db307..fa81f58d9 100644 --- a/providers/dns/vscale/vscale.go +++ b/providers/dns/vscale/vscale.go @@ -4,17 +4,20 @@ package vscale import ( + "context" "errors" "fmt" "net/http" + "net/url" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/internal/selectel" ) +const minTTL = 60 + // Environment variables names. const ( envNamespace = "VSCALE_" @@ -28,20 +31,23 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const defaultBaseURL = "https://api.vscale.io/v1/domains" - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. -type Config = selectel.Config +type Config struct { + BaseURL string + Token string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - BaseURL: env.GetOrDefaultString(EnvBaseURL, defaultBaseURL), - TTL: env.GetOrDefaultInt(EnvTTL, selectel.MinTTL), + BaseURL: env.GetOrDefaultString(EnvBaseURL, selectel.DefaultVScaleBaseURL), + TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, @@ -50,7 +56,8 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - prv challenge.ProviderTimeout + config *Config + client *selectel.Client } // NewDNSProvider returns a DNSProvider instance configured for Vscale Domains API. @@ -73,40 +80,89 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("vscale: the configuration of the DNS provider is nil") } - if config.BaseURL == "" { - config.BaseURL = defaultBaseURL + if config.Token == "" { + return nil, errors.New("vscale: credentials missing") } - provider, err := selectel.NewDNSProviderConfig(config) + if config.TTL < minTTL { + return nil, fmt.Errorf("vscale: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) + } + + client := selectel.NewClient(config.Token) + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + var err error + client.BaseURL, err = url.Parse(config.BaseURL) if err != nil { return nil, fmt.Errorf("vscale: %w", err) } - return &DNSProvider{prv: provider}, nil + return &DNSProvider{config: config, client: client}, nil } -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - err := d.prv.Present(domain, token, keyAuth) - if err != nil { - return fmt.Errorf("vscale: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - err := d.prv.CleanUp(domain, token, keyAuth) - if err != nil { - return fmt.Errorf("vscale: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Timeout returns the Timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.prv.Timeout() + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record to fulfill DNS-01 challenge. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + // TODO(ldez) replace domain by FQDN to follow CNAME. + domainObj, err := d.client.GetDomainByName(ctx, domain) + if err != nil { + return fmt.Errorf("vscale: %w", err) + } + + txtRecord := selectel.Record{ + Type: "TXT", + TTL: d.config.TTL, + Name: info.EffectiveFQDN, + Content: info.Value, + } + _, err = d.client.AddRecord(ctx, domainObj.ID, txtRecord) + if err != nil { + return fmt.Errorf("vscale: %w", err) + } + + return nil +} + +// CleanUp removes a TXT record used for DNS-01 challenge. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + recordName := dns01.UnFqdn(info.EffectiveFQDN) + + ctx := context.Background() + + // TODO(ldez) replace domain by FQDN to follow CNAME. + domainObj, err := d.client.GetDomainByName(ctx, domain) + if err != nil { + return fmt.Errorf("vscale: %w", err) + } + + records, err := d.client.ListRecords(ctx, domainObj.ID) + if err != nil { + return fmt.Errorf("vscale: %w", err) + } + + // Delete records with specific FQDN + var lastErr error + for _, record := range records { + if record.Name == recordName { + err = d.client.DeleteRecord(ctx, domainObj.ID, record.ID) + if err != nil { + lastErr = fmt.Errorf("vscale: %w", err) + } + } + } + + return lastErr } diff --git a/providers/dns/vscale/vscale.toml b/providers/dns/vscale/vscale.toml index f7dc0d943..db69ec784 100644 --- a/providers/dns/vscale/vscale.toml +++ b/providers/dns/vscale/vscale.toml @@ -6,7 +6,7 @@ Since = "v2.0.0" Example = ''' VSCALE_API_TOKEN=xxxxx \ -lego --dns vscale -d '*.example.com' -d example.com run +lego --email you@example.com --dns vscale --domains my.example.org run ''' [Configuration] @@ -14,10 +14,10 @@ lego --dns vscale -d '*.example.com' -d example.com run VSCALE_API_TOKEN = "API token" [Configuration.Additional] VSCALE_BASE_URL = "API endpoint URL" - VSCALE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - VSCALE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - VSCALE_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" - VSCALE_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + VSCALE_POLLING_INTERVAL = "Time between DNS propagation check" + VSCALE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + VSCALE_TTL = "The TTL of the TXT record used for the DNS challenge" + VSCALE_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://developers.vscale.io/documentation/api/v1/#api-Domains_Records" diff --git a/providers/dns/vscale/vscale_test.go b/providers/dns/vscale/vscale_test.go index 9012c7563..6a9b25583 100644 --- a/providers/dns/vscale/vscale_test.go +++ b/providers/dns/vscale/vscale_test.go @@ -6,7 +6,6 @@ import ( "time" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/providers/dns/internal/selectel" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -37,7 +36,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -47,7 +45,8 @@ func TestNewDNSProvider(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - assert.NotNil(t, p.prv) + assert.NotNil(t, p.config) + assert.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } @@ -77,7 +76,7 @@ func TestNewDNSProviderConfig(t *testing.T) { desc: "bad TTL value", token: "123", ttl: 59, - expected: fmt.Sprintf("vscale: invalid TTL, TTL (59) must be greater than %d", selectel.MinTTL), + expected: fmt.Sprintf("vscale: invalid TTL, TTL (59) must be greater than %d", minTTL), }, } @@ -92,7 +91,8 @@ func TestNewDNSProviderConfig(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - assert.NotNil(t, p.prv) + assert.NotNil(t, p.config) + assert.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } @@ -106,7 +106,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -120,7 +119,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/vultr/vultr.go b/providers/dns/vultr/vultr.go index f97a321c1..f63abc5f0 100644 --- a/providers/dns/vultr/vultr.go +++ b/providers/dns/vultr/vultr.go @@ -10,11 +10,9 @@ import ( "strings" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/vultr/govultr/v3" + "github.com/vultr/govultr/v2" "golang.org/x/oauth2" ) @@ -30,8 +28,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string @@ -39,7 +35,7 @@ type Config struct { PollingInterval time.Duration TTL int HTTPClient *http.Client - HTTPTimeout time.Duration // TODO(ldez): remove in v5 + HTTPTimeout time.Duration } // NewDefaultConfig returns a default configuration for the DNSProvider. @@ -85,7 +81,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { authClient := OAuthStaticAccessToken(config.HTTPClient, config.APIKey) authClient.Timeout = config.HTTPTimeout - client := govultr.NewClient(clientdebug.Wrap(authClient)) + client := govultr.NewClient(authClient) return &DNSProvider{client: client, config: config}, nil } @@ -107,17 +103,16 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("vultr: %w", err) } - req := govultr.DomainRecordCreateReq{ + req := govultr.DomainRecordReq{ Name: subDomain, Type: "TXT", Data: `"` + info.Value + `"`, TTL: d.config.TTL, Priority: func(v int) *int { return &v }(0), } - - _, resp, err := d.client.DomainRecord.Create(ctx, zoneDomain, &req) + _, err = d.client.DomainRecord.Create(ctx, zoneDomain, &req) if err != nil { - return fmt.Errorf("vultr: %w", extendError(resp, err)) + return fmt.Errorf("vultr: API call failed: %w", err) } return nil @@ -136,7 +131,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } var allErr []string - for _, rec := range records { err := d.client.DomainRecord.Delete(ctx, zoneDomain, rec.ID) if err != nil { @@ -163,9 +157,9 @@ func (d *DNSProvider) getHostedZone(ctx context.Context, domain string) (string, var hostedDomain govultr.Domain for { - domains, meta, resp, err := d.client.Domain.List(ctx, listOptions) + domains, meta, err := d.client.Domain.List(ctx, listOptions) if err != nil { - return "", extendError(resp, err) + return "", fmt.Errorf("API call failed: %w", err) } for _, dom := range domains { @@ -206,11 +200,10 @@ func (d *DNSProvider) findTxtRecords(ctx context.Context, domain, fqdn string) ( listOptions := &govultr.ListOptions{PerPage: 25} var records []govultr.DomainRecord - for { - result, meta, resp, err := d.client.DomainRecord.List(ctx, zoneDomain, listOptions) + result, meta, err := d.client.DomainRecord.List(ctx, zoneDomain, listOptions) if err != nil { - return "", records, extendError(resp, err) + return "", records, fmt.Errorf("API call has failed: %w", err) } for _, record := range result { @@ -241,12 +234,3 @@ func OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Clien return client } - -func extendError(resp *http.Response, err error) error { - msg := "API call failed" - if resp != nil { - msg += fmt.Sprintf(" (%d)", resp.StatusCode) - } - - return fmt.Errorf("%s: %w", msg, err) -} diff --git a/providers/dns/vultr/vultr.toml b/providers/dns/vultr/vultr.toml index 78e878bea..33483fa62 100644 --- a/providers/dns/vultr/vultr.toml +++ b/providers/dns/vultr/vultr.toml @@ -6,17 +6,17 @@ Since = "v0.3.1" Example = ''' VULTR_API_KEY=xxxxx \ -lego --dns vultr -d '*.example.com' -d example.com run +lego --email you@example.com --dns vultr --domains my.example.org run ''' [Configuration] [Configuration.Credentials] VULTR_API_KEY = "API key" [Configuration.Additional] - VULTR_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - VULTR_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - VULTR_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - VULTR_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + VULTR_POLLING_INTERVAL = "Time between DNS propagation check" + VULTR_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + VULTR_TTL = "The TTL of the TXT record used for the DNS challenge" + VULTR_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.vultr.com/api/#dns" diff --git a/providers/dns/vultr/vultr_test.go b/providers/dns/vultr/vultr_test.go index 17d962b2a..964c09608 100644 --- a/providers/dns/vultr/vultr_test.go +++ b/providers/dns/vultr/vultr_test.go @@ -1,6 +1,7 @@ package vultr import ( + "context" "encoding/json" "fmt" "net/http" @@ -10,10 +11,9 @@ import ( "time" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/vultr/govultr/v3" + "github.com/vultr/govultr/v2" ) const envDomain = envNamespace + "TEST_DOMAIN" @@ -45,7 +45,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -161,53 +160,56 @@ func TestDNSProvider_getHostedZone(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := govultr.NewClient(nil) + err := client.SetBaseURL(server.URL) + require.NoError(t, err) + + p := &DNSProvider{client: client} + var pageCount int - provider := servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - client := govultr.NewClient(server.Client()) - err := client.SetBaseURL(server.URL) - require.NoError(t, err) + mux.HandleFunc("/v2/domains", func(rw http.ResponseWriter, req *http.Request) { + pageCount++ - return &DNSProvider{client: client}, nil - }, - ). - Route("GET /v2/domains", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - pageCount++ + query := req.URL.Query() + cursor, _ := strconv.Atoi(query.Get("cursor")) + perPage, _ := strconv.Atoi(query.Get("per_page")) - query := req.URL.Query() - cursor, _ := strconv.Atoi(query.Get("cursor")) - perPage, _ := strconv.Atoi(query.Get("per_page")) + var next string + if len(domains)/perPage > cursor { + next = strconv.Itoa(cursor + 1) + } - var next string - if len(domains)/perPage > cursor { - next = strconv.Itoa(cursor + 1) - } + start := cursor * perPage + if len(domains) < start { + start = cursor * len(domains) + } - start := cursor * perPage - if len(domains) < start { - start = cursor * len(domains) - } + end := (cursor + 1) * perPage + if len(domains) < end { + end = len(domains) + } - end := min(len(domains), (cursor+1)*perPage) + db := domainsBase{ + Domains: domains[start:end], + Meta: &govultr.Meta{ + Total: len(domains), + Links: &govultr.Links{Next: next}, + }, + } - db := domainsBase{ - Domains: domains[start:end], - Meta: &govultr.Meta{ - Total: len(domains), - Links: &govultr.Links{Next: next}, - }, - } + err = json.NewEncoder(rw).Encode(db) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) - err := json.NewEncoder(rw).Encode(db) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - })). - Build(t) - - zone, err := provider.getHostedZone(t.Context(), test.domain) + zone, err := p.getHostedZone(context.Background(), test.domain) require.NoError(t, err) assert.Equal(t, test.expected, zone) @@ -222,7 +224,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -236,7 +237,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/webnames/internal/client.go b/providers/dns/webnames/internal/client.go index 985503d2a..5b1a8b357 100644 --- a/providers/dns/webnames/internal/client.go +++ b/providers/dns/webnames/internal/client.go @@ -83,7 +83,6 @@ func (c *Client) doRequest(ctx context.Context, data url.Values) error { } var r APIResponse - err = json.Unmarshal(raw, &r) if err != nil { return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) diff --git a/providers/dns/webnames/internal/client_test.go b/providers/dns/webnames/internal/client_test.go index 9507b6f98..8885c50d6 100644 --- a/providers/dns/webnames/internal/client_test.go +++ b/providers/dns/webnames/internal/client_test.go @@ -1,25 +1,75 @@ package internal import ( + "context" + "fmt" + "io" + "net/http" "net/http/httptest" + "net/url" + "os" + "path" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("secret") - client.baseURL = server.URL - client.HTTPClient = server.Client() +func setupTest(t *testing.T, filename string, expectedParams url.Values) *Client { + t.Helper() - return client, nil - }, - servermock.CheckHeader(). - WithContentTypeFromURLEncoded(), - ) + mux := http.NewServeMux() + + mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + if req.Header.Get("Content-Type") != "application/x-www-form-urlencoded" { + http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + err := req.ParseForm() + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + for k, v := range expectedParams { + val := req.PostForm.Get(k) + if len(v) == 0 { + http.Error(rw, fmt.Sprintf("%s: no value", k), http.StatusBadRequest) + return + } + + if val != v[0] { + http.Error(rw, fmt.Sprintf("%s: invalid value: %s != %s", k, val, v[0]), http.StatusBadRequest) + return + } + } + + file, err := os.Open(path.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + server := httptest.NewServer(mux) + + client := NewClient("secret") + client.baseURL = server.URL + client.HTTPClient = server.Client() + + return client } func TestClient_AddTXTRecord(t *testing.T) { @@ -44,23 +94,19 @@ func TestClient_AddTXTRecord(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - client := mockBuilder(). - Route("POST /", - servermock.ResponseFromFixture(test.filename), - servermock.CheckForm().Strict(). - With("domain", "example.com"). - With("type", "TXT"). - With("record", "foo:txtTXTtxt"). - With("action", "add"). - With("apikey", "secret"), - ). - Build(t) + data := url.Values{} + data.Set("domain", "example.com") + data.Set("type", "TXT") + data.Set("record", "foo:txtTXTtxt") + data.Set("action", "add") + + client := setupTest(t, test.filename, data) domain := "example.com" subDomain := "foo" content := "txtTXTtxt" - err := client.AddTXTRecord(t.Context(), domain, subDomain, content) + err := client.AddTXTRecord(context.Background(), domain, subDomain, content) test.require(t, err) }) } @@ -88,23 +134,19 @@ func TestClient_RemoveTxtRecord(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - client := mockBuilder(). - Route("POST /", - servermock.ResponseFromFixture(test.filename), - servermock.CheckForm().Strict(). - With("domain", "example.com"). - With("type", "TXT"). - With("record", "foo:txtTXTtxt"). - With("action", "delete"). - With("apikey", "secret"), - ). - Build(t) + data := url.Values{} + data.Set("domain", "example.com") + data.Set("type", "TXT") + data.Set("record", "foo:txtTXTtxt") + data.Set("action", "delete") + + client := setupTest(t, test.filename, data) domain := "example.com" subDomain := "foo" content := "txtTXTtxt" - err := client.RemoveTXTRecord(t.Context(), domain, subDomain, content) + err := client.RemoveTXTRecord(context.Background(), domain, subDomain, content) test.require(t, err) }) } diff --git a/providers/dns/webnames/webnames.go b/providers/dns/webnames/webnames.go index 9c27164e3..dcc26347e 100644 --- a/providers/dns/webnames/webnames.go +++ b/providers/dns/webnames/webnames.go @@ -6,20 +6,16 @@ import ( "errors" "fmt" "net/http" - "strings" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/webnames/internal" ) // Environment variables names. const ( - envNamespace = "WEBNAMESRU_" - altEnvNamespace = "WEBNAMES_" + envNamespace = "WEBNAMES_" EnvAPIKey = envNamespace + "API_KEY" @@ -28,8 +24,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string @@ -42,10 +36,10 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - PropagationTimeout: env.GetOneWithFallback(EnvPropagationTimeout, dns01.DefaultPropagationTimeout, env.ParseSecond, altEnvName(EnvPropagationTimeout)), - PollingInterval: env.GetOneWithFallback(EnvPollingInterval, dns01.DefaultPollingInterval, env.ParseSecond, altEnvName(EnvPollingInterval)), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ - Timeout: env.GetOneWithFallback(EnvHTTPTimeout, 20*time.Second, env.ParseSecond, altEnvName(EnvHTTPTimeout)), + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } @@ -57,11 +51,11 @@ type DNSProvider struct { } // NewDNSProvider returns a new DNS provider using -// environment variable WEBNAMESRU_API_KEY for adding and removing the DNS record. +// environment variable WEBNAMES_API_KEY for adding and removing the DNS record. func NewDNSProvider() (*DNSProvider, error) { - values, err := env.GetWithFallback([]string{EnvAPIKey, altEnvName(EnvAPIKey)}) + values, err := env.Get(EnvAPIKey) if err != nil { - return nil, fmt.Errorf("webnamesru: %w", err) + return nil, fmt.Errorf("webnames: %w", err) } config := NewDefaultConfig() @@ -73,11 +67,11 @@ func NewDNSProvider() (*DNSProvider, error) { // NewDNSProviderConfig return a DNSProvider instance configured for Webnames. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { - return nil, errors.New("webnamesru: the configuration of the DNS provider is nil") + return nil, errors.New("webnames: the configuration of the DNS provider is nil") } if config.APIKey == "" { - return nil, errors.New("webnamesru: credentials missing") + return nil, errors.New("webnames: credentials missing") } client := internal.NewClient(config.APIKey) @@ -86,8 +80,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{config: config, client: client}, nil } @@ -97,17 +89,17 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { - return fmt.Errorf("webnamesru: could not find zone for domain %q: %w", domain, err) + return fmt.Errorf("webnames: could not find zone for domain %q: %w", domain, err) } subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) if err != nil { - return fmt.Errorf("webnamesru: %w", err) + return fmt.Errorf("webnames: %w", err) } err = d.client.AddTXTRecord(context.Background(), dns01.UnFqdn(authZone), subDomain, info.Value) if err != nil { - return fmt.Errorf("webnamesru: failed to create TXT records [domain: %s, sub domain: %s]: %w", + return fmt.Errorf("webnames: failed to create TXT records [domain: %s, sub domain: %s]: %w", dns01.UnFqdn(authZone), subDomain, err) } @@ -120,17 +112,17 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { - return fmt.Errorf("webnamesru: could not find zone for domain %q: %w", domain, err) + return fmt.Errorf("webnames: could not find zone for domain %q: %w", domain, err) } subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) if err != nil { - return fmt.Errorf("webnamesru: %w", err) + return fmt.Errorf("webnames: %w", err) } err = d.client.RemoveTXTRecord(context.Background(), dns01.UnFqdn(authZone), subDomain, info.Value) if err != nil { - return fmt.Errorf("webnamesru: failed to remove TXT records [domain: %s, sub domain: %s]: %w", + return fmt.Errorf("webnames: failed to remove TXT records [domain: %s, sub domain: %s]: %w", dns01.UnFqdn(authZone), subDomain, err) } @@ -142,7 +134,3 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } - -func altEnvName(v string) string { - return strings.ReplaceAll(v, envNamespace, altEnvNamespace) -} diff --git a/providers/dns/webnames/webnames.toml b/providers/dns/webnames/webnames.toml index b038deaf5..b42ac3e12 100644 --- a/providers/dns/webnames/webnames.toml +++ b/providers/dns/webnames/webnames.toml @@ -1,13 +1,12 @@ -Name = "webnames.ru" +Name = "Webnames" Description = '''''' URL = "https://www.webnames.ru/" Code = "webnames" -Aliases = ["webnamesru"] Since = "v4.15.0" Example = ''' -WEBNAMESRU_API_KEY=xxxxxx \ -lego --dns webnamesru -d '*.example.com' -d example.com run +WEBNAMES_API_KEY=xxxxxx \ +lego --email you@example.com --dns webnames --domains my.example.org run ''' Additional = ''' @@ -20,11 +19,12 @@ The API key can be found: Personal account / My domains and services / Select th [Configuration] [Configuration.Credentials] - WEBNAMESRU_API_KEY = "Domain API key" + WEBNAMES_API_KEY = "Domain API key" [Configuration.Additional] - WEBNAMESRU_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - WEBNAMESRU_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - WEBNAMESRU_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + WEBNAMES_POLLING_INTERVAL = "Time between DNS propagation check" + WEBNAMES_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + WEBNAMES_TTL = "The TTL of the TXT record used for the DNS challenge" + WEBNAMES_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://github.com/regtime-ltd/certbot-dns-webnames" diff --git a/providers/dns/webnames/webnames_test.go b/providers/dns/webnames/webnames_test.go index 072591c68..3ec69501f 100644 --- a/providers/dns/webnames/webnames_test.go +++ b/providers/dns/webnames/webnames_test.go @@ -29,14 +29,13 @@ func TestNewDNSProvider(t *testing.T) { envVars: map[string]string{ EnvAPIKey: "", }, - expected: "webnamesru: some credentials information are missing: WEBNAMESRU_API_KEY", + expected: "webnames: some credentials information are missing: WEBNAMES_API_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -66,7 +65,7 @@ func TestNewDNSProviderConfig(t *testing.T) { }, { desc: "missing credentials", - expected: "webnamesru: credentials missing", + expected: "webnames: credentials missing", }, } @@ -94,7 +93,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -108,7 +106,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/webnamesca/internal/client.go b/providers/dns/webnamesca/internal/client.go deleted file mode 100644 index 203ff9eac..000000000 --- a/providers/dns/webnamesca/internal/client.go +++ /dev/null @@ -1,162 +0,0 @@ -package internal - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - "github.com/go-acme/lego/v4/providers/dns/internal/useragent" -) - -const defaultBaseURL = "https://www.webnames.ca/_/APICore" - -// Client the webnames.ca API client. -type Client struct { - user string - key string - - BaseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(user, key string) (*Client, error) { - if user == "" || key == "" { - return nil, errors.New("credentials missing") - } - - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - user: user, - key: key, - BaseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -func (c *Client) AddTXTRecord(ctx context.Context, domainName, hostName, value string) ([]DNSRecordSet, error) { - endpoint := c.BaseURL.JoinPath("domains", domainName, "add-txt-record") - - query := endpoint.Query() - query.Set("hostName", hostName) - query.Set("txt", value) - - endpoint.RawQuery = query.Encode() - - req, err := newJSONRequest(ctx, http.MethodPost, endpoint, nil) - if err != nil { - return nil, err - } - - var result APIResponse[*DNSInfo] - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return result.Result.DNSRecordSets, nil -} - -func (c *Client) DeleteTXTRecord(ctx context.Context, domainName, hostName, value string) ([]DNSRecordSet, error) { - endpoint := c.BaseURL.JoinPath("domains", domainName, "delete-txt-record") - - query := endpoint.Query() - query.Set("hostName", hostName) - query.Set("txt", value) - - endpoint.RawQuery = query.Encode() - - req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return nil, err - } - - var result APIResponse[*DNSInfo] - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return result.Result.DNSRecordSets, nil -} - -func (c *Client) do(req *http.Request, result any) error { - useragent.SetHeader(req.Header) - - req.Header.Set("API-User", c.user) - req.Header.Set("API-Key", c.key) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - return parseError(req, resp) - } - - if result == nil { - return nil - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - err = json.Unmarshal(raw, result) - if err != nil { - return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) - } - - return nil -} - -func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { - buf := new(bytes.Buffer) - - if payload != nil { - err := json.NewEncoder(buf).Encode(payload) - if err != nil { - return nil, fmt.Errorf("failed to create request JSON body: %w", err) - } - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) - if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) - } - - req.Header.Set("Accept", "application/json") - - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } - - return req, nil -} - -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) - - var errAPI APIError - - err := json.Unmarshal(raw, &errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return &errAPI -} diff --git a/providers/dns/webnamesca/internal/client_test.go b/providers/dns/webnamesca/internal/client_test.go deleted file mode 100644 index ad8571ed0..000000000 --- a/providers/dns/webnamesca/internal/client_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package internal - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("user", "secret") - if err != nil { - return nil, err - } - - client.BaseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }, - servermock.CheckHeader(). - With("API-User", "user"). - With("API-Key", "secret"). - WithJSONHeaders(), - ) -} - -func TestClient_AddTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/example.com/add-txt-record", - servermock.ResponseFromFixture("add_txt_record.json"), - servermock.CheckQueryParameter().Strict(). - With("hostName", "foo.example.com"). - With("txt", "value")). - Build(t) - - result, err := client.AddTXTRecord(t.Context(), "example.com", "foo.example.com", "value") - require.NoError(t, err) - - expected := []DNSRecordSet{{ - Hostname: "_acme-challenge.example.com", - Type: "TXT", - Records: []string{"value"}, - }} - - assert.Equal(t, expected, result) -} - -func TestClient_AddTXTRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /domains/example.com/add-txt-record", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) - - _, err := client.AddTXTRecord(t.Context(), "example.com", "foo.example.com", "value") - require.EqualError(t, err, "message: User does not exist., details: string, logiD: 35579, result: {}") -} - -func TestClient_DeleteTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /domains/example.com/delete-txt-record", - servermock.ResponseFromFixture("delete_txt_record.json"), - servermock.CheckQueryParameter().Strict(). - With("hostName", "foo.example.com"). - With("txt", "value")). - Build(t) - - result, err := client.DeleteTXTRecord(t.Context(), "example.com", "foo.example.com", "value") - require.NoError(t, err) - - expected := []DNSRecordSet{{ - Hostname: "_acme-challenge.example.com", - Type: "TXT", - Records: []string{"value"}, - }} - - assert.Equal(t, expected, result) -} - -func TestClient_DeleteTXTRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /domains/example.com/delete-txt-record", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) - - _, err := client.DeleteTXTRecord(t.Context(), "example.com", "foo.example.com", "value") - require.EqualError(t, err, "message: User does not exist., details: string, logiD: 35579, result: {}") -} diff --git a/providers/dns/webnamesca/internal/fixtures/add_txt_record.json b/providers/dns/webnamesca/internal/fixtures/add_txt_record.json deleted file mode 100644 index 9754689a7..000000000 --- a/providers/dns/webnamesca/internal/fixtures/add_txt_record.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "result": { - "domainAdvancedDNSConfigID": 3258480, - "domainID": 1333334, - "dtCreated": "2025-10-30T11:55:23.243", - "dtModified": "2025-10-30T11:55:23.177", - "timeToLive": 21600, - "soAorigin": "hosting.webnames.ca", - "soArefresh": 21600, - "soAretry": 180, - "soAexpire": 1209600, - "soAnegcache": 3600, - "forwardingURL": null, - "gripping": false, - "name": null, - "dtSubmitted": "2025-10-30T11:55:24.927", - "dtRequestedDNSChange": null, - "type": "REAL_DOMAIN", - "userManaged": false, - "effectiveMgmtOption": "AD", - "urlForwardRootOnly": false, - "enableDNSSEC": false, - "dnsRecordSets": [ - { - "hostname": "_acme-challenge.example.com", - "type": "TXT", - "records": [ - "value" - ] - } - ] - }, - "logID": 36014 -} diff --git a/providers/dns/webnamesca/internal/fixtures/delete_txt_record.json b/providers/dns/webnamesca/internal/fixtures/delete_txt_record.json deleted file mode 100644 index be2279ef6..000000000 --- a/providers/dns/webnamesca/internal/fixtures/delete_txt_record.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "errorMessage": "string", - "errorDetails": "string", - "logID": 0, - "result": { - "domainAdvancedDNSConfigID": 0, - "domainID": 0, - "dtCreated": "2025-10-29T21:22:31.478", - "dtModified": "2025-10-29T21:22:31.478", - "timeToLive": 0, - "soAorigin": "string", - "soArefresh": 0, - "soAretry": 0, - "soAexpire": 0, - "soAnegcache": 0, - "forwardingURL": "string", - "gripping": true, - "name": "string", - "dtSubmitted": "2025-10-29T21:22:31.478", - "dtRequestedDNSChange": "2025-10-29T21:22:31.478", - "type": "string", - "userManaged": true, - "effectiveMgmtOption": "string", - "urlForwardRootOnly": true, - "enableDNSSEC": true, - "dnsRecordSets": [ - { - "hostname": "_acme-challenge.example.com", - "type": "TXT", - "records": [ - "value" - ] - } - ] - } -} diff --git a/providers/dns/webnamesca/internal/fixtures/error.json b/providers/dns/webnamesca/internal/fixtures/error.json deleted file mode 100644 index 3e7548abb..000000000 --- a/providers/dns/webnamesca/internal/fixtures/error.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "errorMessage": "User does not exist.", - "errorDetails": "string", - "logID": 35579, - "result": {} -} diff --git a/providers/dns/webnamesca/internal/types.go b/providers/dns/webnamesca/internal/types.go deleted file mode 100644 index 8dc56c33a..000000000 --- a/providers/dns/webnamesca/internal/types.go +++ /dev/null @@ -1,33 +0,0 @@ -package internal - -import ( - "encoding/json" - "fmt" -) - -type APIError struct { - ErrorMessage string `json:"errorMessage,omitempty"` - ErrorDetails string `json:"errorDetails,omitempty"` - LogID int `json:"logID,omitempty"` - Result json.RawMessage `json:"result,omitempty"` -} - -func (a *APIError) Error() string { - return fmt.Sprintf("message: %s, details: %s, logiD: %d, result: %s", a.ErrorMessage, a.ErrorDetails, a.LogID, a.Result) -} - -type APIResponse[T any] struct { - Result T `json:"result,omitempty"` - LogID int `json:"logID,omitempty"` -} - -type DNSInfo struct { - DomainID int `json:"domainID,omitempty"` - DNSRecordSets []DNSRecordSet `json:"dnsRecordSets,omitempty"` -} - -type DNSRecordSet struct { - Hostname string `json:"hostname"` - Type string `json:"type"` - Records []string `json:"records"` -} diff --git a/providers/dns/webnamesca/webnamesca.go b/providers/dns/webnamesca/webnamesca.go deleted file mode 100644 index 874c1c48e..000000000 --- a/providers/dns/webnamesca/webnamesca.go +++ /dev/null @@ -1,134 +0,0 @@ -// Package webnamesca implements a DNS provider for solving the DNS-01 challenge using webnames.ca. -package webnamesca - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/webnamesca/internal" -) - -// Environment variables names. -const ( - envNamespace = "WEBNAMESCA_" - - EnvAPIUser = envNamespace + "API_USER" - EnvAPIKey = envNamespace + "API_KEY" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - APIUser string - APIKey string - - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for webnames.ca. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIUser, EnvAPIKey) - if err != nil { - return nil, fmt.Errorf("webnamesca: %w", err) - } - - config := NewDefaultConfig() - config.APIUser = values[EnvAPIUser] - config.APIKey = values[EnvAPIKey] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for webnames.ca. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("webnamesca: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.APIUser, config.APIKey) - if err != nil { - return nil, fmt.Errorf("webnamesca: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("webnamesca: could not find zone for domain %q: %w", domain, err) - } - - _, err = d.client.AddTXTRecord(context.Background(), dns01.UnFqdn(authZone), dns01.UnFqdn(info.EffectiveFQDN), info.Value) - if err != nil { - return fmt.Errorf("webnamesca: add TXT record: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) - if err != nil { - return fmt.Errorf("webnamesca: could not find zone for domain %q: %w", domain, err) - } - - _, err = d.client.DeleteTXTRecord(context.Background(), dns01.UnFqdn(authZone), dns01.UnFqdn(info.EffectiveFQDN), info.Value) - if err != nil { - return fmt.Errorf("webnamesca: delete TXT record: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/webnamesca/webnamesca.toml b/providers/dns/webnamesca/webnamesca.toml deleted file mode 100644 index ab68a04a0..000000000 --- a/providers/dns/webnamesca/webnamesca.toml +++ /dev/null @@ -1,24 +0,0 @@ -Name = "webnames.ca" -Description = '''''' -URL = "https://www.webnames.ca/" -Code = "webnamesca" -Since = "v4.28.0" - -Example = ''' -WEBNAMESCA_API_USER="xxx" \ -WEBNAMESCA_API_KEY="yyy" \ -lego --dns webnamesca -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - WEBNAMESCA_API_USER = "API username" - WEBNAMESCA_API_KEY = "API key" - [Configuration.Additional] - WEBNAMESCA_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - WEBNAMESCA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - WEBNAMESCA_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" - WEBNAMESCA_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://www.webnames.ca/_/swagger/index.html" diff --git a/providers/dns/webnamesca/webnamesca_test.go b/providers/dns/webnamesca/webnamesca_test.go deleted file mode 100644 index 0459ef44e..000000000 --- a/providers/dns/webnamesca/webnamesca_test.go +++ /dev/null @@ -1,199 +0,0 @@ -package webnamesca - -import ( - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvAPIUser, EnvAPIKey).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAPIUser: "user", - EnvAPIKey: "secret", - }, - }, - { - desc: "missing EnvAPIUser", - envVars: map[string]string{ - EnvAPIUser: "", - EnvAPIKey: "secret", - }, - expected: "webnamesca: some credentials information are missing: WEBNAMESCA_API_USER", - }, - { - desc: "missing EnvAPIKey", - envVars: map[string]string{ - EnvAPIUser: "user", - EnvAPIKey: "", - }, - expected: "webnamesca: some credentials information are missing: WEBNAMESCA_API_KEY", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "webnamesca: some credentials information are missing: WEBNAMESCA_API_USER,WEBNAMESCA_API_KEY", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - apiUser string - apiKey string - expected string - }{ - { - desc: "success", - apiUser: "user", - apiKey: "secret", - }, - { - desc: "missing apiUser", - apiKey: "secret", - expected: "webnamesca: credentials missing", - }, - { - desc: "missing apiKey", - apiUser: "user", - expected: "webnamesca: credentials missing", - }, - { - desc: "missing credentials", - expected: "webnamesca: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIUser = test.apiUser - config.APIKey = test.apiKey - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func mockBuilder() *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIUser = "user" - config.APIKey = "secret" - config.HTTPClient = server.Client() - - p, err := NewDNSProviderConfig(config) - if err != nil { - return nil, err - } - - p.client.BaseURL, _ = url.Parse(server.URL) - - return p, nil - }, - servermock.CheckHeader(). - WithJSONHeaders(). - With("API-User", "user"). - With("API-Key", "secret"), - ) -} - -func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). - Route("POST /domains/example.com/add-txt-record", - servermock.ResponseFromInternal("add_txt_record.json"), - servermock.CheckQueryParameter().Strict(). - With("hostName", "_acme-challenge.example.com"). - With("txt", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY")). - Build(t) - - err := provider.Present("example.com", "abc", "123d==") - require.NoError(t, err) -} - -func TestDNSProvider_CleanUp(t *testing.T) { - provider := mockBuilder(). - Route("DELETE /domains/example.com/delete-txt-record", - servermock.ResponseFromInternal("delete_txt_record.json"), - servermock.CheckQueryParameter().Strict(). - With("hostName", "_acme-challenge.example.com"). - With("txt", "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY")). - Build(t) - - err := provider.CleanUp("example.com", "abc", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/internal/active24/internal/client.go b/providers/dns/websupport/internal/client.go similarity index 52% rename from providers/dns/internal/active24/internal/client.go rename to providers/dns/websupport/internal/client.go index 69e94b367..cc40e9dea 100644 --- a/providers/dns/internal/active24/internal/client.go +++ b/providers/dns/websupport/internal/client.go @@ -12,118 +12,152 @@ import ( "io" "net/http" "net/url" + "strconv" "time" "github.com/go-acme/lego/v4/providers/dns/internal/errutils" ) -const defaultBaseURL = "https://rest.%s" +const defaultBaseURL = "https://rest.websupport.sk" -// Client the Active24 API client. +// StatusSuccess expected status text when success. +const StatusSuccess = "success" + +// Client a Websupport DNS API client. type Client struct { - apiKey string - secret string + apiKey string + secretKey string baseURL *url.URL HTTPClient *http.Client } // NewClient creates a new Client. -func NewClient(baseAPIDomain, apiKey, secret string) (*Client, error) { - if apiKey == "" || secret == "" { +func NewClient(apiKey, secretKey string) (*Client, error) { + if apiKey == "" || secretKey == "" { return nil, errors.New("credentials missing") } - baseURL, _ := url.Parse(fmt.Sprintf(defaultBaseURL, baseAPIDomain)) + baseURL, _ := url.Parse(defaultBaseURL) return &Client{ apiKey: apiKey, - secret: secret, + secretKey: secretKey, baseURL: baseURL, HTTPClient: &http.Client{Timeout: 10 * time.Second}, }, nil } -// GetServices lists of all services. -// https://rest.active24.cz/docs/v1.service#services -func (c *Client) GetServices(ctx context.Context) ([]Service, error) { - endpoint := c.baseURL.JoinPath("v1", "user", "self", "service") +// GetUser gets a user detail. +// https://rest.websupport.sk/docs/v1.user#user +func (c *Client) GetUser(ctx context.Context, userID string) (*User, error) { + endpoint := c.baseURL.JoinPath("v1", "user", userID) + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, fmt.Errorf("request payload: %w", err) + } + + result := &User{} + + err = c.do(req, result) + if err != nil { + return nil, err + } + + return result, nil +} + +// ListRecords lists all records. +// https://rest.websupport.sk/docs/v1.zone#records +func (c *Client) ListRecords(ctx context.Context, domainName string) (*ListResponse, error) { + endpoint := c.baseURL.JoinPath("v1", "user", "self", "zone", domainName, "record") + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, fmt.Errorf("request payload: %w", err) + } + + result := &ListResponse{} + + err = c.do(req, result) + if err != nil { + return nil, err + } + + return result, nil +} + +// GetRecords gets a DNS record. +func (c *Client) GetRecords(ctx context.Context, domainName string, recordID int) (*Record, error) { + endpoint := c.baseURL.JoinPath("v1", "user", "self", "zone", domainName, "record", strconv.Itoa(recordID)) req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } - var result OldAPIResponse + result := &Record{} - err = c.do(req, &result) + err = c.do(req, result) if err != nil { return nil, err } - return result.Items, err + return result, nil } -// GetRecords lists of DNS records. -// https://rest.active24.cz/v2/docs#/DNS/rest.v2.dns.record_f94908d4e0e48489468498fce87cb90b -func (c *Client) GetRecords(ctx context.Context, service string, filter RecordFilter) ([]Record, error) { - endpoint := c.baseURL.JoinPath("v2", "service", service, "dns", "record") - - encodedFilter, err := json.Marshal(filter) - if err != nil { - return nil, fmt.Errorf("marshal records filter: %w", err) - } - - query := endpoint.Query() - query.Add("filters", string(encodedFilter)) - - req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, err - } - - var result APIResponse - - err = c.do(req, &result) - if err != nil { - return nil, err - } - - return result.Data, err -} - -// CreateRecord creates a new DNS record. -// https://rest.active24.cz/v2/docs#/DNS/rest.v2.dns.create-record_6773d572235be9a72646bf6c54863573 -func (c *Client) CreateRecord(ctx context.Context, service string, record Record) error { - endpoint := c.baseURL.JoinPath("v2", "service", service, "dns", "record") +// AddRecord adds a DNS record. +// https://rest.websupport.sk/docs/v1.zone#post-record +func (c *Client) AddRecord(ctx context.Context, domainName string, record Record) (*Response, error) { + endpoint := c.baseURL.JoinPath("v1", "user", "self", "zone", domainName, "record") req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) if err != nil { - return err + return nil, fmt.Errorf("create request: %w", err) } - return c.do(req, nil) + result := &Response{} + + err = c.do(req, result) + if err != nil { + return nil, err + } + + return result, nil } // DeleteRecord deletes a DNS record. -// https://rest.active24.cz/v2/docs#/DNS/rest.v2.dns.delete-record_fc6603c14848e547f8d0b967842f0a2c -func (c *Client) DeleteRecord(ctx context.Context, service, recordID string) error { - endpoint := c.baseURL.JoinPath("v2", "service", service, "dns", "record", recordID) +// https://rest.websupport.sk/docs/v1.zone#delete-record +func (c *Client) DeleteRecord(ctx context.Context, domainName string, recordID int) (*Response, error) { + endpoint := c.baseURL.JoinPath("v1", "user", "self", "zone", domainName, "record", strconv.Itoa(recordID)) req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) if err != nil { - return err + return nil, fmt.Errorf("create request: %w", err) } - return c.do(req, nil) + result := &Response{} + + err = c.do(req, result) + if err != nil { + return nil, err + } + + return result, nil } func (c *Client) do(req *http.Request, result any) error { req.Header.Set("Accept-Language", "en_us") - err := c.sign(req, time.Now()) + location, err := time.LoadLocation("GMT") if err != nil { - return fmt.Errorf("sign request: %w", err) + return fmt.Errorf("time location: %w", err) + } + + err = c.sign(req, time.Now().In(location)) + if err != nil { + return fmt.Errorf("signature: %w", err) } resp, err := c.HTTPClient.Do(req) @@ -133,14 +167,10 @@ func (c *Client) do(req *http.Request, result any) error { defer func() { _ = resp.Body.Close() }() - if resp.StatusCode/100 != 2 { + if resp.StatusCode > http.StatusBadRequest { return parseError(req, resp) } - if result == nil { - return nil - } - raw, err := io.ReadAll(resp.Body) if err != nil { return errutils.NewReadResponseError(req, resp.StatusCode, err) @@ -154,6 +184,29 @@ func (c *Client) do(req *http.Request, result any) error { return nil } +func (c *Client) sign(req *http.Request, now time.Time) error { + if req.URL.Path == "" { + req.URL.Path += "/" + } + + canonicalRequest := fmt.Sprintf("%s %s %d", req.Method, req.URL.Path, now.Unix()) + + mac := hmac.New(sha1.New, []byte(c.secretKey)) + _, err := mac.Write([]byte(canonicalRequest)) + if err != nil { + return err + } + + hashed := mac.Sum(nil) + signature := hex.EncodeToString(hashed) + + req.SetBasicAuth(c.apiKey, signature) + + req.Header.Set("Date", now.Format(time.RFC3339)) + + return nil +} + func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { buf := new(bytes.Buffer) @@ -182,7 +235,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) var errAPI APIError - err := json.Unmarshal(raw, &errAPI) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) @@ -190,29 +242,3 @@ func parseError(req *http.Request, resp *http.Response) error { return &errAPI } - -// sign creates and sets request signature and date. -// https://rest.active24.cz/v2/docs/intro -func (c *Client) sign(req *http.Request, now time.Time) error { - if req.URL.Path == "" { - req.URL.Path += "/" - } - - canonicalRequest := fmt.Sprintf("%s %s %d", req.Method, req.URL.Path, now.Unix()) - - mac := hmac.New(sha1.New, []byte(c.secret)) - - _, err := mac.Write([]byte(canonicalRequest)) - if err != nil { - return err - } - - hashed := mac.Sum(nil) - signature := hex.EncodeToString(hashed) - - req.SetBasicAuth(c.apiKey, signature) - - req.Header.Set("Date", now.Format(time.RFC3339)) - - return nil -} diff --git a/providers/dns/websupport/internal/client_test.go b/providers/dns/websupport/internal/client_test.go new file mode 100644 index 000000000..9612f6096 --- /dev/null +++ b/providers/dns/websupport/internal/client_test.go @@ -0,0 +1,234 @@ +package internal + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupTest(t *testing.T, method, pattern string, status int, file string) *Client { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + open, err := os.Open(file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = open.Close() }() + + rw.WriteHeader(status) + _, err = io.Copy(rw, open) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + client, err := NewClient("apiKey", "secretKey") + require.NoError(t, err) + + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client +} + +func TestClient_GetUser(t *testing.T) { + client := setupTest(t, http.MethodGet, "/v1/user/self", http.StatusOK, "./fixtures/get-user.json") + + user, err := client.GetUser(context.Background(), "self") + require.NoError(t, err) + + expected := &User{ + ID: 987654321, + Login: "lego@example.com", + Active: true, + CreateTime: 1675237889, + Group: "users", + Email: "lego@example.com", + Phone: "+123456789", + ContactPerson: "", + AwaitingTosConfirmation: "1", + UserLanguage: "sk-SK", + Credit: 0, + VerifyURL: "https://rest.websupport.sk/v1/user/verify/key/xxx", + Billing: []Billing{{ + ID: 1099970, + Profile: "default", + IsDefault: true, + Name: "asdsdfs", + City: "Žilina", + Street: "asddfsdfsdf", + Zip: "01234", + Country: "sk", + }}, + Market: Market{Name: "Slovakia", Identifier: "sk", Currency: "EUR"}, + } + + assert.Equal(t, expected, user) +} + +func TestClient_ListRecords(t *testing.T) { + client := setupTest(t, http.MethodGet, "/v1/user/self/zone/example.com/record", http.StatusOK, "./fixtures/list-records.json") + + resp, err := client.ListRecords(context.Background(), "example.com") + require.NoError(t, err) + + expected := &ListResponse{ + Items: []Record{ + { + ID: 1, + Type: "A", + Name: "@", + Content: "37.9.169.99", + TTL: 600, + }, { + ID: 2, + Type: "NS", + Name: "@", + Content: "ns1.scaledo.com", + TTL: 600, + }, + }, + Pager: Pager{Page: 1, PageSize: 0, Items: 2}, + } + + assert.Equal(t, expected, resp) +} + +func TestClient_AddRecord(t *testing.T) { + client := setupTest(t, http.MethodPost, "/v1/user/self/zone/example.com/record", http.StatusCreated, "./fixtures/add-record.json") + + record := Record{ + Type: "TXT", + Name: "_acme-challenge", + Content: "txttxttxt", + TTL: 600, + } + + resp, err := client.AddRecord(context.Background(), "example.com", record) + require.NoError(t, err) + + expected := &Response{ + Status: "success", + Item: &Record{ + ID: 4, + Type: "A", + Name: "@", + Content: "1.2.3.4", + TTL: 600, + Zone: &Zone{ + ID: 1, + Name: "example.com", + UpdateTime: 1381169608, + }, + }, + Errors: json.RawMessage("[]"), + } + + assert.Equal(t, expected, resp) +} + +func TestClient_AddRecord_error_400(t *testing.T) { + client := setupTest(t, http.MethodPost, "/v1/user/self/zone/example.com/record", http.StatusBadRequest, "./fixtures/add-record-error-400.json") + + record := Record{ + Type: "TXT", + Name: "_acme-challenge", + Content: "txttxttxt", + TTL: 600, + } + + resp, err := client.AddRecord(context.Background(), "example.com", record) + require.NoError(t, err) + + assert.Equal(t, "error", resp.Status) + + expectedRecord := &Record{ + ID: 0, + Type: "A", + Name: "something bad !@#$%^&*(", + Content: "123.456.789.123", + TTL: 600, + Zone: &Zone{ + ID: 1, + Name: "scaledo.com", + UpdateTime: 1381169608, + }, + } + assert.Equal(t, expectedRecord, resp.Item) + + expected := &Errors{Name: []string{"Invalid input."}, Content: []string{"Wrong IP address format"}} + assert.Equal(t, expected, ParseError(resp)) +} + +func TestClient_AddRecord_error_404(t *testing.T) { + client := setupTest(t, http.MethodPost, "/v1/user/self/zone/example.com/record", http.StatusNotFound, "./fixtures/add-record-error-404.json") + + record := Record{ + Type: "TXT", + Name: "_acme-challenge", + Content: "txttxttxt", + TTL: 600, + } + + resp, err := client.AddRecord(context.Background(), "example.com", record) + require.Error(t, err) + + assert.Nil(t, resp) +} + +func TestClient_DeleteRecord(t *testing.T) { + client := setupTest(t, http.MethodDelete, "/v1/user/self/zone/example.com/record/123", http.StatusOK, "./fixtures/delete-record.json") + + resp, err := client.DeleteRecord(context.Background(), "example.com", 123) + require.NoError(t, err) + + expected := &Response{ + Status: "success", + Item: &Record{ + ID: 1, + Type: "A", + Name: "@", + Content: "1.2.3.4", + TTL: 600, + Zone: &Zone{ + ID: 1, + Name: "scaledo.com", + UpdateTime: 1381316081, + }, + }, + Errors: json.RawMessage("[]"), + } + + assert.Equal(t, expected, resp) +} + +func TestClient_DeleteRecord_error(t *testing.T) { + client := setupTest(t, http.MethodDelete, "/v1/user/self/zone/example.com/record/123", http.StatusNotFound, "./fixtures/delete-record-error-404.json") + + resp, err := client.DeleteRecord(context.Background(), "example.com", 123) + require.Error(t, err) + + assert.Nil(t, resp) +} diff --git a/providers/dns/websupport/internal/fixtures/add-record-error-400.json b/providers/dns/websupport/internal/fixtures/add-record-error-400.json new file mode 100644 index 000000000..b60b7989a --- /dev/null +++ b/providers/dns/websupport/internal/fixtures/add-record-error-400.json @@ -0,0 +1,26 @@ +{ + "status": "error", + "item": { + "id": null, + "type": "A", + "name": "something bad !@#$%^&*(", + "content": "123.456.789.123", + "ttl": 600, + "prio": null, + "weight": null, + "port": null, + "zone": { + "id": 1, + "name": "scaledo.com", + "updateTime": 1381169608 + } + }, + "errors": { + "content": [ + "Wrong IP address format" + ], + "name": [ + "Invalid input." + ] + } +} diff --git a/providers/dns/websupport/internal/fixtures/add-record-error-404.json b/providers/dns/websupport/internal/fixtures/add-record-error-404.json new file mode 100644 index 000000000..837b5392a --- /dev/null +++ b/providers/dns/websupport/internal/fixtures/add-record-error-404.json @@ -0,0 +1,4 @@ +{ + "code": 404, + "message": "Zone not found" +} diff --git a/providers/dns/websupport/internal/fixtures/add-record.json b/providers/dns/websupport/internal/fixtures/add-record.json new file mode 100644 index 000000000..5990cf3d3 --- /dev/null +++ b/providers/dns/websupport/internal/fixtures/add-record.json @@ -0,0 +1,19 @@ +{ + "status": "success", + "item": { + "id": 4, + "type": "A", + "name": "@", + "content": "1.2.3.4", + "ttl": 600, + "prio": null, + "weight": null, + "port": null, + "zone": { + "id": 1, + "name": "example.com", + "updateTime": 1381169608 + } + }, + "errors": [] +} diff --git a/providers/dns/websupport/internal/fixtures/delete-record-error-404.json b/providers/dns/websupport/internal/fixtures/delete-record-error-404.json new file mode 100644 index 000000000..e66fa5dc6 --- /dev/null +++ b/providers/dns/websupport/internal/fixtures/delete-record-error-404.json @@ -0,0 +1,4 @@ +{ + "code": 404, + "message": "Record not found" +} diff --git a/providers/dns/websupport/internal/fixtures/delete-record.json b/providers/dns/websupport/internal/fixtures/delete-record.json new file mode 100644 index 000000000..8fdff82cb --- /dev/null +++ b/providers/dns/websupport/internal/fixtures/delete-record.json @@ -0,0 +1,19 @@ +{ + "status": "success", + "item": { + "id": 1, + "type": "A", + "name": "@", + "content": "1.2.3.4", + "ttl": 600, + "prio": null, + "weight": null, + "port": null, + "zone": { + "id": 1, + "name": "scaledo.com", + "updateTime": 1381316081 + } + }, + "errors": [] +} diff --git a/providers/dns/websupport/internal/fixtures/get-record.json b/providers/dns/websupport/internal/fixtures/get-record.json new file mode 100644 index 000000000..d1bd2f137 --- /dev/null +++ b/providers/dns/websupport/internal/fixtures/get-record.json @@ -0,0 +1,12 @@ +{ + "id": 69966832, + "type": "TXT", + "name": "_acme-challenge", + "content": "txttxttxt", + "ttl": 600, + "zone": { + "id": 0, + "name": "example.com", + "updateTime": 1675240207 + } +} diff --git a/providers/dns/websupport/internal/fixtures/get-user.json b/providers/dns/websupport/internal/fixtures/get-user.json new file mode 100644 index 000000000..ad4978755 --- /dev/null +++ b/providers/dns/websupport/internal/fixtures/get-user.json @@ -0,0 +1,36 @@ +{ + "id": 987654321, + "login": "lego@example.com", + "parentId": null, + "active": true, + "createTime": 1675237889, + "group": "users", + "email": "lego@example.com", + "phone": "+123456789", + "contactPerson": "", + "awaitingTosConfirmation": "1", + "userLanguage": "sk-SK", + "credit": 0, + "verifyUrl": "https:\/\/rest.websupport.sk\/v1\/user\/verify\/key\/xxx", + "billing": [ + { + "id": 1099970, + "profile": "default", + "isDefault": true, + "name": "asdsdfs", + "city": "\u017dilina", + "street": "asddfsdfsdf", + "companyRegId": null, + "taxId": null, + "vatId": null, + "zip": "01234", + "country": "sk", + "isic": "" + } + ], + "market": { + "name": "Slovakia", + "identifier": "sk", + "currency": "EUR" + } +} diff --git a/providers/dns/websupport/internal/fixtures/list-records.json b/providers/dns/websupport/internal/fixtures/list-records.json new file mode 100644 index 000000000..d0ad57dc9 --- /dev/null +++ b/providers/dns/websupport/internal/fixtures/list-records.json @@ -0,0 +1,29 @@ +{ + "items": [ + { + "id": 1, + "type": "A", + "name": "@", + "content": "37.9.169.99", + "ttl": 600, + "prio": null, + "weight": null, + "port": null + }, + { + "id": 2, + "type": "NS", + "name": "@", + "content": "ns1.scaledo.com", + "ttl": 600, + "prio": null, + "weight": null, + "port": null + } + ], + "pager": { + "page": 1, + "pagesize": 0, + "items": 2 + } +} diff --git a/providers/dns/websupport/internal/types.go b/providers/dns/websupport/internal/types.go new file mode 100644 index 000000000..0923282aa --- /dev/null +++ b/providers/dns/websupport/internal/types.go @@ -0,0 +1,121 @@ +package internal + +import ( + "encoding/json" + "fmt" +) + +type APIError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +func (a *APIError) Error() string { + return fmt.Sprintf("%d: %s", a.Code, a.Message) +} + +type Record struct { + ID int `json:"id,omitempty"` + Type string `json:"type,omitempty"` + Name string `json:"name,omitempty"` // subdomain name or @ if you don't want subdomain + Content string `json:"content,omitempty"` + TTL int `json:"ttl,omitempty"` // default 600 + Zone *Zone `json:"zone"` +} + +type Zone struct { + ID int `json:"id"` + Name string `json:"name"` + UpdateTime int `json:"updateTime"` +} + +type Response struct { + Item *Record `json:"item"` + Status string `json:"status"` + Errors json.RawMessage `json:"errors"` +} + +type ListResponse struct { + Items []Record `json:"items"` + Pager Pager `json:"pager"` +} + +type Pager struct { + Page int `json:"page"` + PageSize int `json:"pagesize"` + Items int `json:"items"` +} + +type Errors struct { + Name []string `json:"name"` + Content []string `json:"content"` +} + +func (e *Errors) Error() string { + var msg string + for i, s := range e.Name { + msg += s + if i != len(e.Name)-1 { + msg += ": " + } + } + + for i, s := range e.Content { + msg += s + if i != len(e.Content)-1 { + msg += ": " + } + } + + return msg +} + +// ParseError extract error from Response. +func ParseError(resp *Response) error { + var errAPI Errors + err := json.Unmarshal(resp.Errors, &errAPI) + if err != nil { + return err + } + + return &errAPI +} + +type User struct { + ID int `json:"id"` + Login string `json:"login"` + ParentID int `json:"parentId"` + Active bool `json:"active"` + CreateTime int `json:"createTime"` + Group string `json:"group"` + Email string `json:"email"` + Phone string `json:"phone"` + ContactPerson string `json:"contactPerson"` + AwaitingTosConfirmation string `json:"awaitingTosConfirmation"` + UserLanguage string `json:"userLanguage"` + Credit int `json:"credit"` + VerifyURL string `json:"verifyUrl"` + Billing []Billing `json:"billing"` + Market Market `json:"market"` +} + +type Billing struct { + ID int `json:"id"` + Profile string `json:"profile"` + IsDefault bool `json:"isDefault"` + Name string `json:"name"` + City string `json:"city"` + Street string `json:"street"` + CompanyRegID int `json:"companyRegId"` + TaxID int `json:"taxId"` + VatID int `json:"vatId"` + Zip string `json:"zip"` + Country string `json:"country"` + ISIC string `json:"isic"` +} + +type Market struct { + Name string `json:"name"` + Identifier string `json:"identifier"` + Currency string `json:"currency"` +} diff --git a/providers/dns/websupport/websupport.go b/providers/dns/websupport/websupport.go index 4187ba32b..3a257b425 100644 --- a/providers/dns/websupport/websupport.go +++ b/providers/dns/websupport/websupport.go @@ -2,19 +2,18 @@ package websupport import ( + "context" "errors" "fmt" "net/http" + "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/active24" + "github.com/go-acme/lego/v4/providers/dns/websupport/internal" ) -const baseAPIDomain = "websupport.sk" - // Environment variables names. const ( envNamespace = "WEBSUPPORT_" @@ -26,17 +25,28 @@ const ( EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" + EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. -type Config = active24.Config +type Config struct { + APIKey string + Secret string + + PropagationTimeout time.Duration + PollingInterval time.Duration + SequenceInterval time.Duration + TTL int + HTTPClient *http.Client +} // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + TTL: env.GetOrDefaultInt(EnvTTL, 600), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, @@ -45,7 +55,11 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - prv challenge.ProviderTimeout + config *Config + client *internal.Client + + recordIDs map[string]int + recordIDsMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance configured for Websupport. @@ -69,36 +83,101 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("websupport: the configuration of the DNS provider is nil") } - provider, err := active24.NewDNSProviderConfig(config, baseAPIDomain) + client, err := internal.NewClient(config.APIKey, config.Secret) if err != nil { return nil, fmt.Errorf("websupport: %w", err) } - return &DNSProvider{prv: provider}, nil + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + return &DNSProvider{ + config: config, + client: client, + recordIDs: make(map[string]int), + }, nil } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - err := d.prv.Present(domain, token, keyAuth) + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("websupport: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) if err != nil { return fmt.Errorf("websupport: %w", err) } - return nil + record := internal.Record{ + Type: "TXT", + Name: subDomain, + Content: info.Value, + TTL: d.config.TTL, + } + + resp, err := d.client.AddRecord(context.Background(), dns01.UnFqdn(authZone), record) + if err != nil { + return fmt.Errorf("websupport: add record: %w", err) + } + + if resp.Status == internal.StatusSuccess { + d.recordIDsMu.Lock() + d.recordIDs[token] = resp.Item.ID + d.recordIDsMu.Unlock() + + return nil + } + + return fmt.Errorf("websupport: %w", internal.ParseError(resp)) } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - err := d.prv.CleanUp(domain, token, keyAuth) + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { - return fmt.Errorf("websupport: %w", err) + return fmt.Errorf("websupport: could not find zone for domain %q: %w", domain, err) } - return nil + // gets the record's unique ID + d.recordIDsMu.Lock() + recordID, ok := d.recordIDs[token] + d.recordIDsMu.Unlock() + if !ok { + return fmt.Errorf("websupport: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) + } + + resp, err := d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), recordID) + if err != nil { + return fmt.Errorf("websupport: delete record: %w", err) + } + + // deletes record ID from map + d.recordIDsMu.Lock() + delete(d.recordIDs, token) + d.recordIDsMu.Unlock() + + if resp.Status == internal.StatusSuccess { + return nil + } + + return fmt.Errorf("websupport: %w", internal.ParseError(resp)) } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.prv.Timeout() + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Sequential All DNS challenges for this provider will be resolved sequentially. +// Returns the interval between each iteration. +func (d *DNSProvider) Sequential() time.Duration { + return d.config.SequenceInterval } diff --git a/providers/dns/websupport/websupport.toml b/providers/dns/websupport/websupport.toml index 4908f0235..8eb32fbbb 100644 --- a/providers/dns/websupport/websupport.toml +++ b/providers/dns/websupport/websupport.toml @@ -7,7 +7,7 @@ Since = "v4.10.0" Example = ''' WEBSUPPORT_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ WEBSUPPORT_SECRET="yyyyyyyyyyyyyyyyyyyyy" \ -lego --dns websupport -d '*.example.com' -d example.com run +lego --email myemail@example.com --dns websupport --domains my.example.org run ''' [Configuration] @@ -15,12 +15,11 @@ lego --dns websupport -d '*.example.com' -d example.com run WEBSUPPORT_API_KEY = "API key" WEBSUPPORT_SECRET = "API secret" [Configuration.Additional] - WEBSUPPORT_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - WEBSUPPORT_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - WEBSUPPORT_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 60)" - WEBSUPPORT_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)" - WEBSUPPORT_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + WEBSUPPORT_POLLING_INTERVAL = "Time between DNS propagation check" + WEBSUPPORT_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + WEBSUPPORT_SEQUENCE_INTERVAL = "Time between sequential requests" + WEBSUPPORT_TTL = "The TTL of the TXT record used for the DNS challenge" + WEBSUPPORT_HTTP_TIMEOUT = "API request timeout" [Links] - API = "https://rest.websupport.sk/v2/docs" - APIv1 = "https://rest.websupport.sk/docs/v1.service#services" + API = "https://rest.websupport.sk/docs/v1.zone" diff --git a/providers/dns/websupport/websupport_test.go b/providers/dns/websupport/websupport_test.go index 196c9bab8..e79dd7130 100644 --- a/providers/dns/websupport/websupport_test.go +++ b/providers/dns/websupport/websupport_test.go @@ -20,14 +20,13 @@ func TestNewDNSProvider(t *testing.T) { { desc: "success", envVars: map[string]string{ - EnvAPIKey: "user", + EnvAPIKey: "key", EnvSecret: "secret", }, }, { desc: "missing API key", envVars: map[string]string{ - EnvAPIKey: "", EnvSecret: "secret", }, expected: "websupport: some credentials information are missing: WEBSUPPORT_API_KEY", @@ -35,8 +34,7 @@ func TestNewDNSProvider(t *testing.T) { { desc: "missing secret", envVars: map[string]string{ - EnvAPIKey: "user", - EnvSecret: "", + EnvAPIKey: "key", }, expected: "websupport: some credentials information are missing: WEBSUPPORT_SECRET", }, @@ -50,7 +48,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -60,7 +57,8 @@ func TestNewDNSProvider(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - require.NotNil(t, p.prv) + require.NotNil(t, p.config) + require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } @@ -77,19 +75,17 @@ func TestNewDNSProviderConfig(t *testing.T) { }{ { desc: "success", - apiKey: "user", + apiKey: "key", secret: "secret", }, { desc: "missing API key", - apiKey: "", secret: "secret", expected: "websupport: credentials missing", }, { desc: "missing secret", - apiKey: "user", - secret: "", + apiKey: "key", expected: "websupport: credentials missing", }, { @@ -109,7 +105,8 @@ func TestNewDNSProviderConfig(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - require.NotNil(t, p.prv) + require.NotNil(t, p.config) + require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } @@ -123,7 +120,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -137,7 +133,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/wedos/internal/client.go b/providers/dns/wedos/internal/client.go index 48c89d189..defcabf6c 100644 --- a/providers/dns/wedos/internal/client.go +++ b/providers/dns/wedos/internal/client.go @@ -26,7 +26,7 @@ type Client struct { } // NewClient creates a new Client. -func NewClient(username, password string) *Client { +func NewClient(username string, password string) *Client { return &Client{ username: username, password: password, @@ -69,7 +69,6 @@ func (c *Client) AddRecord(ctx context.Context, zone string, record DNSRow) erro } cmd := commandDNSRowAdd - if record.ID == "" { payload.Name = record.Name } else { @@ -88,7 +87,7 @@ func (c *Client) AddRecord(ctx context.Context, zone string, record DNSRow) erro // DeleteRecord deletes a record from the zone. // If a record does not have an ID, it will be looked up. // https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-row-delete/ -func (c *Client) DeleteRecord(ctx context.Context, zone, recordID string) error { +func (c *Client) DeleteRecord(ctx context.Context, zone string, recordID string) error { payload := DNSRowRequest{ Domain: dns01.UnFqdn(zone), ID: recordID, diff --git a/providers/dns/wedos/internal/client_test.go b/providers/dns/wedos/internal/client_test.go index f2515618a..30c7d4863 100644 --- a/providers/dns/wedos/internal/client_test.go +++ b/providers/dns/wedos/internal/client_test.go @@ -1,38 +1,64 @@ package internal import ( + "context" "fmt" "net/http" "net/http/httptest" + "os" "regexp" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("user", "secret") - client.baseURL = server.URL - client.HTTPClient = server.Client() +func setupNew(t *testing.T, expectedForm string, filename string) *Client { + t.Helper() - return client, nil - }, - servermock.CheckHeader(). - WithContentTypeFromURLEncoded()) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { + err := req.ParseForm() + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + exp := regexp.MustCompile(`"auth":"\w+",`) + + form := req.PostForm.Get("request") + form = exp.ReplaceAllString(form, `"auth":"xxx",`) + + if form != expectedForm { + t.Logf("invalid form data: %s", req.PostForm.Get("request")) + http.Error(rw, fmt.Sprintf("invalid form data: %s", req.PostForm.Get("request")), http.StatusBadRequest) + return + } + + data, err := os.ReadFile(fmt.Sprintf("./fixtures/%s.json", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + rw.Header().Set("Content-Type", "application/json") + _, _ = rw.Write(data) + }) + + client := NewClient("user", "secret") + client.baseURL = server.URL + + return client } func TestClient_GetRecords(t *testing.T) { - client := mockBuilder(). - Route("POST /", - servermock.ResponseFromFixture(commandDNSRowsList+".json"), - checkFormRequest(`{"request":{"user":"user","auth":"xxx","command":"dns-rows-list","data":{"domain":"example.com"}}}`)). - Build(t) + expectedForm := `{"request":{"user":"user","auth":"xxx","command":"dns-rows-list","data":{"domain":"example.com"}}}` + client := setupNew(t, expectedForm, commandDNSRowsList) - records, err := client.GetRecords(t.Context(), "example.com.") + records, err := client.GetRecords(context.Background(), "example.com.") require.NoError(t, err) assert.Len(t, records, 4) @@ -69,11 +95,9 @@ func TestClient_GetRecords(t *testing.T) { } func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /", - servermock.ResponseFromFixture(commandDNSRowAdd+".json"), - checkFormRequest(`{"request":{"user":"user","auth":"xxx","command":"dns-row-add","data":{"domain":"example.com","name":"foo","ttl":1800,"type":"TXT","rdata":"foobar"}}}`)). - Build(t) + expectedForm := `{"request":{"user":"user","auth":"xxx","command":"dns-row-add","data":{"domain":"example.com","name":"foo","ttl":1800,"type":"TXT","rdata":"foobar"}}}` + + client := setupNew(t, expectedForm, commandDNSRowAdd) record := DNSRow{ ID: "", @@ -83,16 +107,14 @@ func TestClient_AddRecord(t *testing.T) { Data: "foobar", } - err := client.AddRecord(t.Context(), "example.com.", record) + err := client.AddRecord(context.Background(), "example.com.", record) require.NoError(t, err) } func TestClient_AddRecord_update(t *testing.T) { - client := mockBuilder(). - Route("POST /", - servermock.ResponseFromFixture(commandDNSRowUpdate+".json"), - checkFormRequest(`{"request":{"user":"user","auth":"xxx","command":"dns-row-update","data":{"row_id":"1","domain":"example.com","ttl":1800,"type":"TXT","rdata":"foobar"}}}`)). - Build(t) + expectedForm := `{"request":{"user":"user","auth":"xxx","command":"dns-row-update","data":{"row_id":"1","domain":"example.com","ttl":1800,"type":"TXT","rdata":"foobar"}}}` + + client := setupNew(t, expectedForm, commandDNSRowUpdate) record := DNSRow{ ID: "1", @@ -102,50 +124,24 @@ func TestClient_AddRecord_update(t *testing.T) { Data: "foobar", } - err := client.AddRecord(t.Context(), "example.com.", record) + err := client.AddRecord(context.Background(), "example.com.", record) require.NoError(t, err) } func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /", - servermock.ResponseFromFixture(commandDNSRowDelete+".json"), - checkFormRequest(`{"request":{"user":"user","auth":"xxx","command":"dns-row-delete","data":{"row_id":"1","domain":"example.com","rdata":""}}}`)). - Build(t) + expectedForm := `{"request":{"user":"user","auth":"xxx","command":"dns-row-delete","data":{"row_id":"1","domain":"example.com","rdata":""}}}` - err := client.DeleteRecord(t.Context(), "example.com.", "1") + client := setupNew(t, expectedForm, commandDNSRowDelete) + + err := client.DeleteRecord(context.Background(), "example.com.", "1") require.NoError(t, err) } func TestClient_Commit(t *testing.T) { - client := mockBuilder(). - Route("POST /", - servermock.ResponseFromFixture(commandDNSDomainCommit+".json"), - checkFormRequest(`{"request":{"user":"user","auth":"xxx","command":"dns-domain-commit","data":{"name":"example.com"}}}`)). - Build(t) + expectedForm := `{"request":{"user":"user","auth":"xxx","command":"dns-domain-commit","data":{"name":"example.com"}}}` - err := client.Commit(t.Context(), "example.com.") + client := setupNew(t, expectedForm, commandDNSDomainCommit) + + err := client.Commit(context.Background(), "example.com.") require.NoError(t, err) } - -func checkFormRequest(data string) servermock.LinkFunc { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - err := req.ParseForm() - if err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) - return - } - - form := regexp.MustCompile(`"auth":"\w+",`). - ReplaceAllString(req.PostForm.Get("request"), `"auth":"xxx",`) - - if form != data { - http.Error(rw, fmt.Sprintf("invalid form data: %s", req.PostForm.Get("request")), http.StatusBadRequest) - return - } - - next.ServeHTTP(rw, req) - }) - } -} diff --git a/providers/dns/wedos/internal/token.go b/providers/dns/wedos/internal/token.go index 11e680cb8..b83b107c1 100644 --- a/providers/dns/wedos/internal/token.go +++ b/providers/dns/wedos/internal/token.go @@ -8,14 +8,13 @@ import ( "time" ) -func authToken(userName, wapiPass string) string { +func authToken(userName string, wapiPass string) string { return sha1string(userName + sha1string(wapiPass) + czechHourString()) } func sha1string(txt string) string { h := sha1.New() _, _ = io.WriteString(h, txt) - return hex.EncodeToString(h.Sum(nil)) } @@ -47,19 +46,18 @@ func utcToCet(utc time.Time) time.Time { if utcMonth < time.March || utcMonth > time.October { return utc.Add(time.Hour) } - if utcMonth > time.March && utcMonth < time.October { return utc.Add(time.Hour * 2) } dayOff := 0 - breaking := time.Date(utc.Year(), utcMonth+1, dayOff, 1, 0, 0, 0, time.UTC) - for breaking.Weekday() != time.Sunday { + for { + if breaking.Weekday() == time.Sunday { + break + } dayOff-- - breaking = time.Date(utc.Year(), utcMonth+1, dayOff, 1, 0, 0, 0, time.UTC) - if dayOff < -7 { panic("safety exit to avoid infinite loop") } @@ -68,7 +66,6 @@ func utcToCet(utc time.Time) time.Time { if (utcMonth == time.March && utc.Before(breaking)) || (utcMonth == time.October && utc.After(breaking)) { return utc.Add(time.Hour) } - return utc.Add(time.Hour * 2) } diff --git a/providers/dns/wedos/wedos.go b/providers/dns/wedos/wedos.go index 164fb5f10..0004c49f8 100644 --- a/providers/dns/wedos/wedos.go +++ b/providers/dns/wedos/wedos.go @@ -9,10 +9,8 @@ import ( "strconv" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/wedos/internal" ) @@ -31,8 +29,6 @@ const ( const minTTL = 5 * 60 // 5 minutes -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Username string @@ -95,8 +91,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{config: config, client: client}, nil } diff --git a/providers/dns/wedos/wedos.toml b/providers/dns/wedos/wedos.toml index 89abfc16c..cb2693ee5 100644 --- a/providers/dns/wedos/wedos.toml +++ b/providers/dns/wedos/wedos.toml @@ -7,7 +7,7 @@ Since = "v4.4.0" Example = ''' WEDOS_USERNAME=xxxxxxxx \ WEDOS_WAPI_PASSWORD=xxxxxxxx \ -lego --dns wedos -d '*.example.com' -d example.com run +lego --email you@example.com --dns wedos --domains my.example.org run ''' [Configuration] @@ -15,10 +15,10 @@ lego --dns wedos -d '*.example.com' -d example.com run WEDOS_USERNAME = "Username is the same as for the admin account" WEDOS_WAPI_PASSWORD = "Password needs to be generated and IP allowed in the admin interface" [Configuration.Additional] - WEDOS_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" - WEDOS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 600)" - WEDOS_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)" - WEDOS_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + WEDOS_POLLING_INTERVAL = "Time between DNS propagation check" + WEDOS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + WEDOS_HTTP_TIMEOUT = "API request timeout" + WEDOS_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://kb.wedos.com/en/kategorie/wapi-api-interface/wdns-en/" diff --git a/providers/dns/wedos/wedos_test.go b/providers/dns/wedos/wedos_test.go index 25f70d0fc..9363002b5 100644 --- a/providers/dns/wedos/wedos_test.go +++ b/providers/dns/wedos/wedos_test.go @@ -54,7 +54,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -121,7 +120,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -135,7 +133,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/westcn/westcn.go b/providers/dns/westcn/westcn.go deleted file mode 100644 index 1906f9737..000000000 --- a/providers/dns/westcn/westcn.go +++ /dev/null @@ -1,104 +0,0 @@ -// Package westcn implements a DNS provider for solving the DNS-01 challenge using West.cn/西部数码. -package westcn - -import ( - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/westcn" -) - -// Environment variables names. -const ( - envNamespace = "WESTCN_" - - EnvUsername = envNamespace + "USERNAME" - EnvPassword = envNamespace + "PASSWORD" - - EnvTTL = envNamespace + "TTL" - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -const defaultBaseURL = "https://api.west.cn/api/v2" - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - -// Config is used to configure the creation of the DNSProvider. -type Config = westcn.Config - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, 60), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - prv challenge.ProviderTimeout -} - -// NewDNSProvider returns a DNSProvider instance configured for West.cn/西部数码. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvUsername, EnvPassword) - if err != nil { - return nil, fmt.Errorf("westcn: %w", err) - } - - config := NewDefaultConfig() - config.Username = values[EnvUsername] - config.Password = values[EnvPassword] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for West.cn/西部数码. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("westcn: the configuration of the DNS provider is nil") - } - - provider, err := westcn.NewDNSProviderConfig(config, defaultBaseURL) - if err != nil { - return nil, fmt.Errorf("westcn: %w", err) - } - - return &DNSProvider{prv: provider}, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - err := d.prv.Present(domain, token, keyAuth) - if err != nil { - return fmt.Errorf("westcn: %w", err) - } - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - err := d.prv.CleanUp(domain, token, keyAuth) - if err != nil { - return fmt.Errorf("westcn: %w", err) - } - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.prv.Timeout() -} diff --git a/providers/dns/westcn/westcn.toml b/providers/dns/westcn/westcn.toml deleted file mode 100644 index 1b0cb0a7a..000000000 --- a/providers/dns/westcn/westcn.toml +++ /dev/null @@ -1,24 +0,0 @@ -Name = "West.cn/西部数码" -Description = '''''' -URL = "https://www.west.cn" -Code = "westcn" -Since = "v4.21.0" - -Example = ''' -WESTCN_USERNAME="xxx" \ -WESTCN_PASSWORD="yyy" \ -lego --dns westcn -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - WESTCN_USERNAME = "Username" - WESTCN_PASSWORD = "API password" - [Configuration.Additional] - WESTCN_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 10)" - WESTCN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 120)" - WESTCN_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" - WESTCN_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://www.west.cn/CustomerCenter/doc/domain_v2.html" diff --git a/providers/dns/westcn/westcn_test.go b/providers/dns/westcn/westcn_test.go deleted file mode 100644 index a546d518e..000000000 --- a/providers/dns/westcn/westcn_test.go +++ /dev/null @@ -1,144 +0,0 @@ -package westcn - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvUsername, EnvPassword).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvUsername: "user", - EnvPassword: "secret", - }, - }, - { - desc: "missing username", - envVars: map[string]string{ - EnvUsername: "", - EnvPassword: "secret", - }, - expected: "westcn: some credentials information are missing: WESTCN_USERNAME", - }, - { - desc: "missing password", - envVars: map[string]string{ - EnvUsername: "user", - EnvPassword: "", - }, - expected: "westcn: some credentials information are missing: WESTCN_PASSWORD", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "westcn: some credentials information are missing: WESTCN_USERNAME,WESTCN_PASSWORD", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.prv) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - username string - password string - expected string - }{ - { - desc: "success", - username: "user", - password: "secret", - }, - { - desc: "missing username", - password: "secret", - expected: "westcn: credentials missing", - }, - { - desc: "missing password", - username: "user", - expected: "westcn: credentials missing", - }, - { - desc: "missing credentials", - expected: "westcn: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.Username = test.username - config.Password = test.password - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.prv) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/yandex/internal/client.go b/providers/dns/yandex/internal/client.go index 4b0421f49..5d7e6bff3 100644 --- a/providers/dns/yandex/internal/client.go +++ b/providers/dns/yandex/internal/client.go @@ -12,7 +12,7 @@ import ( "time" "github.com/go-acme/lego/v4/providers/dns/internal/errutils" - querystring "github.com/google/go-querystring/query" + "github.com/google/go-querystring/query" ) const defaultBaseURL = "https://pddimp.yandex.ru/api2/admin/dns" @@ -51,7 +51,6 @@ func (c *Client) AddRecord(ctx context.Context, payload Record) (*Record, error) } r := AddResponse{} - err = c.do(req, &r) if err != nil { return nil, err @@ -69,7 +68,6 @@ func (c *Client) RemoveRecord(ctx context.Context, payload Record) (int, error) } r := RemoveResponse{} - err = c.do(req, &r) if err != nil { return 0, err @@ -91,7 +89,6 @@ func (c *Client) GetRecords(ctx context.Context, domain string) ([]Record, error } r := ListResponse{} - err = c.do(req, &r) if err != nil { return nil, err @@ -133,7 +130,7 @@ func newRequest(ctx context.Context, method string, endpoint *url.URL, payload a if payload != nil { switch method { case http.MethodPost: - values, err := querystring.Values(payload) + values, err := query.Values(payload) if err != nil { return nil, err } @@ -141,7 +138,7 @@ func newRequest(ctx context.Context, method string, endpoint *url.URL, payload a buf.WriteString(values.Encode()) case http.MethodGet: - values, err := querystring.Values(payload) + values, err := query.Values(payload) if err != nil { return nil, err } diff --git a/providers/dns/yandex/internal/client_test.go b/providers/dns/yandex/internal/client_test.go index 4bb3357a6..67166ee85 100644 --- a/providers/dns/yandex/internal/client_test.go +++ b/providers/dns/yandex/internal/client_test.go @@ -1,133 +1,328 @@ package internal import ( + "context" + "encoding/json" + "net/http" "net/http/httptest" "net/url" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func setupClient(server *httptest.Server) (*Client, error) { +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + client, err := NewClient("lego") - if err != nil { - return nil, err - } + require.NoError(t, err) client.HTTPClient = server.Client() client.baseURL, _ = url.Parse(server.URL) - return client, nil + return client, mux } func TestAddRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("POST /add", - servermock.ResponseFromFixture("add_record.json"), - servermock.CheckHeader(). - WithContentTypeFromURLEncoded(), - servermock.CheckForm().Strict(). - With("domain", "example.com"). - With("subdomain", "foo"). - With("ttl", "300"). - With("content", "txtTXTtxtTXTtxtTXT"). - With("type", "TXT")). - Build(t) + testCases := []struct { + desc string + handler http.HandlerFunc + data Record + expectError bool + }{ + { + desc: "success", + handler: func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "lego", r.Header.Get(pddTokenHeader)) - data := Record{ - Domain: "example.com", - Type: "TXT", - Content: "txtTXTtxtTXTtxtTXT", - SubDomain: "foo", - TTL: 300, + err := r.ParseForm() + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + assert.Equal(t, `content=txtTXTtxtTXTtxtTXT&domain=example.com&subdomain=foo&ttl=300&type=TXT`, r.PostForm.Encode()) + + response := AddResponse{ + Domain: "example.com", + Record: &Record{ + ID: 1, + Type: "TXT", + Domain: "example.com", + SubDomain: "foo", + FQDN: "foo.example.com.", + Content: "txtTXTtxtTXTtxtTXT", + TTL: 300, + }, + BaseResponse: BaseResponse{ + Success: "ok", + }, + } + + err = json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }, + data: Record{ + Domain: "example.com", + Type: "TXT", + Content: "txtTXTtxtTXTtxtTXT", + SubDomain: "foo", + TTL: 300, + }, + }, + { + desc: "error", + handler: func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "lego", r.Header.Get(pddTokenHeader)) + + err := r.ParseForm() + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + assert.Equal(t, `content=txtTXTtxtTXTtxtTXT&domain=example.com&subdomain=foo&ttl=300&type=TXT`, r.PostForm.Encode()) + + response := AddResponse{ + Domain: "example.com", + BaseResponse: BaseResponse{ + Success: "error", + Error: "bad things", + }, + } + + err = json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }, + data: Record{ + Domain: "example.com", + Type: "TXT", + Content: "txtTXTtxtTXTtxtTXT", + SubDomain: "foo", + TTL: 300, + }, + expectError: true, + }, } - record, err := client.AddRecord(t.Context(), data) - require.NoError(t, err) - require.NotNil(t, record) -} + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + client, mux := setupTest(t) -func TestAddRecord_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("POST /add", - servermock.ResponseFromFixture("add_record_error.json"), - servermock.CheckHeader(). - WithContentTypeFromURLEncoded()). - Build(t) + mux.HandleFunc("/add", test.handler) - data := Record{ - Domain: "example.com", - Type: "TXT", - Content: "txtTXTtxtTXTtxtTXT", - SubDomain: "foo", - TTL: 300, + record, err := client.AddRecord(context.Background(), test.data) + if test.expectError { + require.Error(t, err) + require.Nil(t, record) + } else { + require.NoError(t, err) + require.NotNil(t, record) + } + }) } - - _, err := client.AddRecord(t.Context(), data) - require.EqualError(t, err, "error during operation: error bad things") } func TestRemoveRecord(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("POST /del", - servermock.ResponseFromFixture("remove_record.json"), - servermock.CheckHeader(). - WithContentTypeFromURLEncoded(), - servermock.CheckForm().Strict(). - With("domain", "example.com"). - With("record_id", "6")). - Build(t) + testCases := []struct { + desc string + handler http.HandlerFunc + data Record + expectError bool + }{ + { + desc: "success", + handler: func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "lego", r.Header.Get(pddTokenHeader)) - data := Record{ - ID: 6, - Domain: "example.com", + err := r.ParseForm() + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + assert.Equal(t, `domain=example.com&record_id=6`, r.PostForm.Encode()) + + response := RemoveResponse{ + Domain: "example.com", + RecordID: 6, + BaseResponse: BaseResponse{ + Success: "ok", + }, + } + + err = json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }, + data: Record{ + ID: 6, + Domain: "example.com", + }, + }, + { + desc: "error", + handler: func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "lego", r.Header.Get(pddTokenHeader)) + + err := r.ParseForm() + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + assert.Equal(t, `domain=example.com&record_id=6`, r.PostForm.Encode()) + + response := RemoveResponse{ + Domain: "example.com", + RecordID: 6, + BaseResponse: BaseResponse{ + Success: "error", + Error: "bad things", + }, + } + + err = json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }, + data: Record{ + ID: 6, + Domain: "example.com", + }, + expectError: true, + }, } - id, err := client.RemoveRecord(t.Context(), data) - require.NoError(t, err) + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + client, mux := setupTest(t) - assert.Equal(t, 6, id) -} + mux.HandleFunc("/del", test.handler) -func TestRemoveRecord_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("POST /del", - servermock.ResponseFromFixture("remove_record_error.json"), - servermock.CheckHeader(). - WithContentTypeFromURLEncoded()). - Build(t) - - data := Record{ - ID: 6, - Domain: "example.com", + id, err := client.RemoveRecord(context.Background(), test.data) + if test.expectError { + require.Error(t, err) + require.Equal(t, 0, id) + } else { + require.NoError(t, err) + require.Equal(t, 6, id) + } + }) } - - _, err := client.RemoveRecord(t.Context(), data) - require.EqualError(t, err, "error during operation: error bad things") } func TestGetRecords(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /list", - servermock.ResponseFromFixture("get_records.json"), - servermock.CheckForm().Strict(). - With("domain", "example.com")). - Build(t) + testCases := []struct { + desc string + handler http.HandlerFunc + domain string + expectError bool + }{ + { + desc: "success", + handler: func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, "lego", r.Header.Get(pddTokenHeader)) - records, err := client.GetRecords(t.Context(), "example.com") - require.NoError(t, err) + assert.Equal(t, "domain=example.com", r.URL.RawQuery) - require.Len(t, records, 2) -} - -func TestGetRecords_error(t *testing.T) { - client := servermock.NewBuilder[*Client](setupClient). - Route("GET /list", - servermock.ResponseFromFixture("get_records_error.json")). - Build(t) - - _, err := client.GetRecords(t.Context(), "example.com") - require.EqualError(t, err, "error during operation: error bad things") + response := ListResponse{ + Domain: "example.com", + Records: []Record{ + { + ID: 1, + Type: "TXT", + Domain: "example.com", + SubDomain: "foo", + FQDN: "foo.example.com.", + Content: "txtTXTtxtTXTtxtTXT", + TTL: 300, + }, + { + ID: 2, + Type: "NS", + Domain: "example.com", + SubDomain: "foo", + FQDN: "foo.example.com.", + Content: "bar", + TTL: 300, + }, + }, + BaseResponse: BaseResponse{ + Success: "ok", + }, + } + + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }, + domain: "example.com", + }, + { + desc: "error", + handler: func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, "lego", r.Header.Get(pddTokenHeader)) + + assert.Equal(t, "domain=example.com", r.URL.RawQuery) + + response := ListResponse{ + Domain: "example.com", + BaseResponse: BaseResponse{ + Success: "error", + Error: "bad things", + }, + } + + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }, + domain: "example.com", + expectError: true, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + client, mux := setupTest(t) + + mux.HandleFunc("/list", test.handler) + + records, err := client.GetRecords(context.Background(), test.domain) + if test.expectError { + require.Error(t, err) + require.Empty(t, records) + } else { + require.NoError(t, err) + require.Len(t, records, 2) + } + }) + } } diff --git a/providers/dns/yandex/internal/fixtures/add_record.json b/providers/dns/yandex/internal/fixtures/add_record.json deleted file mode 100644 index 1e4452d1d..000000000 --- a/providers/dns/yandex/internal/fixtures/add_record.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "success": "ok", - "domain": "example.com", - "record": { - "record_id": 1, - "domain": "example.com", - "subdomain": "foo", - "fqdn": "foo.example.com.", - "ttl": 300, - "type": "TXT", - "content": "txtTXTtxtTXTtxtTXT" - } -} diff --git a/providers/dns/yandex/internal/fixtures/add_record_error.json b/providers/dns/yandex/internal/fixtures/add_record_error.json deleted file mode 100644 index 932ccd674..000000000 --- a/providers/dns/yandex/internal/fixtures/add_record_error.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "success": "error", - "error": "bad things", - "domain": "example.com" -} diff --git a/providers/dns/yandex/internal/fixtures/get_records.json b/providers/dns/yandex/internal/fixtures/get_records.json deleted file mode 100644 index e538834b4..000000000 --- a/providers/dns/yandex/internal/fixtures/get_records.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "success": "ok", - "domain": "example.com", - "records": [ - { - "record_id": 1, - "domain": "example.com", - "subdomain": "foo", - "fqdn": "foo.example.com.", - "ttl": 300, - "type": "TXT", - "content": "txtTXTtxtTXTtxtTXT" - }, - { - "record_id": 2, - "domain": "example.com", - "subdomain": "foo", - "fqdn": "foo.example.com.", - "ttl": 300, - "type": "NS", - "content": "bar" - } - ] -} diff --git a/providers/dns/yandex/internal/fixtures/get_records_error.json b/providers/dns/yandex/internal/fixtures/get_records_error.json deleted file mode 100644 index 932ccd674..000000000 --- a/providers/dns/yandex/internal/fixtures/get_records_error.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "success": "error", - "error": "bad things", - "domain": "example.com" -} diff --git a/providers/dns/yandex/internal/fixtures/remove_record.json b/providers/dns/yandex/internal/fixtures/remove_record.json deleted file mode 100644 index 3241ba9dc..000000000 --- a/providers/dns/yandex/internal/fixtures/remove_record.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "success": "ok", - "domain": "example.com", - "record_id": 6 -} diff --git a/providers/dns/yandex/internal/fixtures/remove_record_error.json b/providers/dns/yandex/internal/fixtures/remove_record_error.json deleted file mode 100644 index cd1471c9d..000000000 --- a/providers/dns/yandex/internal/fixtures/remove_record_error.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "success": "error", - "error": "bad things", - "domain": "example.com", - "record_id": 6 -} diff --git a/providers/dns/yandex/internal/types.go b/providers/dns/yandex/internal/types.go index 48a85042c..ed1873cef 100644 --- a/providers/dns/yandex/internal/types.go +++ b/providers/dns/yandex/internal/types.go @@ -30,21 +30,18 @@ func (r BaseResponse) GetError() string { type AddResponse struct { BaseResponse - Domain string `json:"domain,omitempty"` Record *Record `json:"record,omitempty"` } type RemoveResponse struct { BaseResponse - Domain string `json:"domain,omitempty"` RecordID int `json:"record_id,omitempty"` } type ListResponse struct { BaseResponse - Domain string `json:"domain,omitempty"` Records []Record `json:"records,omitempty"` } diff --git a/providers/dns/yandex/yandex.go b/providers/dns/yandex/yandex.go index 7ae505ec0..2886a0333 100644 --- a/providers/dns/yandex/yandex.go +++ b/providers/dns/yandex/yandex.go @@ -8,10 +8,8 @@ import ( "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/yandex/internal" "github.com/miekg/dns" ) @@ -28,8 +26,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { PddToken string @@ -89,8 +85,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{client: client, config: config}, nil } @@ -136,7 +130,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } var record *internal.Record - for _, rcd := range records { if rcd.Type == "TXT" && rcd.SubDomain == subDomain && rcd.Content == info.Value { record = &rcd @@ -157,7 +150,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("yandex: %w", err) } - return nil } diff --git a/providers/dns/yandex/yandex.toml b/providers/dns/yandex/yandex.toml index a36df069e..d52ce4eac 100644 --- a/providers/dns/yandex/yandex.toml +++ b/providers/dns/yandex/yandex.toml @@ -7,17 +7,17 @@ Since = "v3.7.0" Example = ''' YANDEX_PDD_TOKEN= \ -lego --dns yandex -d '*.example.com' -d example.com run +lego --email you@example.com --dns yandex --domains my.example.org run ''' [Configuration] [Configuration.Credentials] YANDEX_PDD_TOKEN = "Basic authentication username" [Configuration.Additional] - YANDEX_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - YANDEX_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - YANDEX_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 21600)" - YANDEX_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + YANDEX_POLLING_INTERVAL = "Time between DNS propagation check" + YANDEX_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + YANDEX_HTTP_TIMEOUT = "API request timeout" + YANDEX_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://yandex.com/dev/domain/doc/concepts/api-dns.html" diff --git a/providers/dns/yandex/yandex_test.go b/providers/dns/yandex/yandex_test.go index 8a0a7534a..144a24126 100644 --- a/providers/dns/yandex/yandex_test.go +++ b/providers/dns/yandex/yandex_test.go @@ -33,7 +33,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -96,7 +95,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -110,7 +108,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/yandex360/internal/client.go b/providers/dns/yandex360/internal/client.go index 33aeb0daa..2bebc6c20 100644 --- a/providers/dns/yandex360/internal/client.go +++ b/providers/dns/yandex360/internal/client.go @@ -47,7 +47,7 @@ func NewClient(oauthToken string, orgID int64) (*Client, error) { // AddRecord Adds a DNS record. // POST https://api30.yandex.net/directory/v1/org/{orgId}/domains/{domain}/dns // https://yandex.ru/dev/api360/doc/ref/DomainDNSService/DomainDNSService_Create.html -func (c *Client) AddRecord(ctx context.Context, domain string, record Record) (*Record, error) { +func (c Client) AddRecord(ctx context.Context, domain string, record Record) (*Record, error) { endpoint := c.baseURL.JoinPath("directory", "v1", "org", strconv.FormatInt(c.orgID, 10), "domains", domain, "dns") req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) @@ -68,7 +68,7 @@ func (c *Client) AddRecord(ctx context.Context, domain string, record Record) (* // DeleteRecord Deletes a DNS record. // DELETE https://api360.yandex.net/directory/v1/org/{orgId}/domains/{domain}/dns/{recordId} // https://yandex.ru/dev/api360/doc/ref/DomainDNSService/DomainDNSService_Delete.html -func (c *Client) DeleteRecord(ctx context.Context, domain string, recordID int64) error { +func (c Client) DeleteRecord(ctx context.Context, domain string, recordID int64) error { endpoint := c.baseURL.JoinPath("directory", "v1", "org", strconv.FormatInt(c.orgID, 10), "domains", domain, "dns", strconv.FormatInt(recordID, 10)) req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) @@ -79,7 +79,7 @@ func (c *Client) DeleteRecord(ctx context.Context, domain string, recordID int64 return c.do(req, nil) } -func (c *Client) do(req *http.Request, result any) error { +func (c Client) do(req *http.Request, result any) error { req.Header.Set("Authorization", "OAuth "+c.oauthToken) resp, err := c.HTTPClient.Do(req) @@ -138,7 +138,6 @@ func parseError(req *http.Request, resp *http.Response) error { raw, _ := io.ReadAll(resp.Body) var apiErr APIError - err := json.Unmarshal(raw, &apiErr) if err != nil { return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) diff --git a/providers/dns/yandex360/internal/client_test.go b/providers/dns/yandex360/internal/client_test.go index aa21672e4..d0ddac0c3 100644 --- a/providers/dns/yandex360/internal/client_test.go +++ b/providers/dns/yandex360/internal/client_test.go @@ -1,39 +1,60 @@ package internal import ( + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client, err := NewClient("secret", 123456) - if err != nil { - return nil, err - } +func setupTest(t *testing.T, pattern, method string, status int, filename string) *Client { + t.Helper() - client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithAuthorization("OAuth secret")) + mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + open, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = open.Close() }() + + rw.WriteHeader(status) + _, err = io.Copy(rw, open) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + client, err := NewClient("secret", 123456) + require.NoError(t, err) + + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client } func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /directory/v1/org/123456/domains/example.com/dns", - servermock.ResponseFromFixture("add-record.json"), - servermock.CheckRequestJSONBody(`{"name":"_acme-challenge","text":"txtxtxt","ttl":60,"type":"TXT"}`)). - Build(t) + client := setupTest(t, "/directory/v1/org/123456/domains/example.com/dns", http.MethodPost, http.StatusOK, "add-record.json") record := Record{ Name: "_acme-challenge", @@ -42,7 +63,7 @@ func TestClient_AddRecord(t *testing.T) { Type: "TXT", } - newRecord, err := client.AddRecord(t.Context(), "example.com", record) + newRecord, err := client.AddRecord(context.Background(), "example.com", record) require.NoError(t, err) expected := &Record{ @@ -57,11 +78,7 @@ func TestClient_AddRecord(t *testing.T) { } func TestClient_AddRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /directory/v1/org/123456/domains/example.com/dns", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client := setupTest(t, "/directory/v1/org/123456/domains/example.com/dns", http.MethodGet, http.StatusUnauthorized, "error.json") record := Record{ Name: "_acme-challenge", @@ -70,29 +87,22 @@ func TestClient_AddRecord_error(t *testing.T) { Type: "TXT", } - newRecord, err := client.AddRecord(t.Context(), "example.com", record) + newRecord, err := client.AddRecord(context.Background(), "example.com", record) require.Error(t, err) assert.Nil(t, newRecord) } func TestClient_DeleteRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /directory/v1/org/123456/domains/example.com/dns/789456", - servermock.ResponseFromFixture("delete-record.json")). - Build(t) + client := setupTest(t, "/directory/v1/org/123456/domains/example.com/dns/789456", http.MethodDelete, http.StatusOK, "delete-record.json") - err := client.DeleteRecord(t.Context(), "example.com", 789456) + err := client.DeleteRecord(context.Background(), "example.com", 789456) require.NoError(t, err) } func TestClient_DeleteRecord_error(t *testing.T) { - client := mockBuilder(). - Route("DELETE /directory/v1/org/123456/domains/example.com/dns/789456", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client := setupTest(t, "/directory/v1/org/123456/domains/example.com/dns/789456", http.MethodDelete, http.StatusUnauthorized, "error.json") - err := client.DeleteRecord(t.Context(), "example.com", 789456) + err := client.DeleteRecord(context.Background(), "example.com", 789456) require.Error(t, err) } diff --git a/providers/dns/yandex360/yandex360.go b/providers/dns/yandex360/yandex360.go index 0f4571750..38aa835d8 100644 --- a/providers/dns/yandex360/yandex360.go +++ b/providers/dns/yandex360/yandex360.go @@ -10,12 +10,9 @@ import ( "sync" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/yandex360/internal" - "github.com/miekg/dns" ) // Environment variables names. @@ -31,8 +28,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { OAuthToken string @@ -99,8 +94,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ client: client, config: config, @@ -112,7 +105,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) - authZone, err := dns01.FindZoneByFqdn(dns.Fqdn(info.EffectiveFQDN)) + authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(info.EffectiveFQDN)) if err != nil { return fmt.Errorf("yandex360: could not find zone for domain %q: %w", domain, err) } @@ -147,7 +140,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) - authZone, err := dns01.FindZoneByFqdn(dns.Fqdn(info.EffectiveFQDN)) + authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(info.EffectiveFQDN)) if err != nil { return fmt.Errorf("yandex360: could not find zone for domain %q: %w", domain, err) } diff --git a/providers/dns/yandex360/yandex360.toml b/providers/dns/yandex360/yandex360.toml index 444b1cc38..ad0ce0d3e 100644 --- a/providers/dns/yandex360/yandex360.toml +++ b/providers/dns/yandex360/yandex360.toml @@ -8,7 +8,7 @@ Since = "v4.14.0" Example = ''' YANDEX360_OAUTH_TOKEN= \ YANDEX360_ORG_ID= \ -lego --dns yandex360 -d '*.example.com' -d example.com run +lego --email you@example.com --dns yandex360 --domains my.example.org run ''' [Configuration] @@ -16,10 +16,10 @@ lego --dns yandex360 -d '*.example.com' -d example.com run YANDEX360_OAUTH_TOKEN = "The OAuth Token" YANDEX360_ORG_ID = "The organization ID" [Configuration.Additional] - YANDEX360_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - YANDEX360_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - YANDEX360_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 21600)" - YANDEX360_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + YANDEX360_POLLING_INTERVAL = "Time between DNS propagation check" + YANDEX360_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + YANDEX360_HTTP_TIMEOUT = "API request timeout" + YANDEX360_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://yandex.ru/dev/api360/doc/ref/DomainDNSService.html" diff --git a/providers/dns/yandex360/yandex360_test.go b/providers/dns/yandex360/yandex360_test.go index c1d37ad12..545c90985 100644 --- a/providers/dns/yandex360/yandex360_test.go +++ b/providers/dns/yandex360/yandex360_test.go @@ -43,7 +43,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -110,7 +109,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -124,7 +122,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/yandexcloud/yandexcloud.go b/providers/dns/yandexcloud/yandexcloud.go index f9c64def1..7a5d0bbed 100644 --- a/providers/dns/yandexcloud/yandexcloud.go +++ b/providers/dns/yandexcloud/yandexcloud.go @@ -11,15 +11,11 @@ import ( "strings" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - ycdnsproto "github.com/yandex-cloud/go-genproto/yandex/cloud/dns/v1" - ycdns "github.com/yandex-cloud/go-sdk/services/dns/v1" - ycsdk "github.com/yandex-cloud/go-sdk/v2" - "github.com/yandex-cloud/go-sdk/v2/credentials" - "github.com/yandex-cloud/go-sdk/v2/pkg/iamkey" - "github.com/yandex-cloud/go-sdk/v2/pkg/options" + ycdns "github.com/yandex-cloud/go-genproto/yandex/cloud/dns/v1" + ycsdk "github.com/yandex-cloud/go-sdk" + "github.com/yandex-cloud/go-sdk/iamkey" ) // Environment variables names. @@ -34,8 +30,6 @@ const ( EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { IamToken string @@ -57,7 +51,7 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - client ycdns.DnsZoneClient + client *ycsdk.SDK config *Config } @@ -94,19 +88,19 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, fmt.Errorf("yandexcloud: iam token is malformed: %w", err) } - sdk, err := ycsdk.Build(context.Background(), options.WithCredentials(creds)) + client, err := ycsdk.Build(context.Background(), ycsdk.Config{Credentials: creds}) if err != nil { return nil, errors.New("yandexcloud: unable to build yandex cloud sdk") } return &DNSProvider{ - client: ycdns.NewDnsZoneClient(sdk), + client: client, config: config, }, nil } // Present creates a TXT record to fulfill the dns-01 challenge. -func (d *DNSProvider) Present(domain, _, keyAuth string) error { +func (r *DNSProvider) Present(domain, _, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) @@ -116,7 +110,7 @@ func (d *DNSProvider) Present(domain, _, keyAuth string) error { ctx := context.Background() - zones, err := d.getZones(ctx) + zones, err := r.getZones(ctx) if err != nil { return fmt.Errorf("yandexcloud: %w", err) } @@ -138,7 +132,7 @@ func (d *DNSProvider) Present(domain, _, keyAuth string) error { return fmt.Errorf("yandexcloud: %w", err) } - err = d.upsertRecordSetData(ctx, zoneID, subDomain, info.Value) + err = r.upsertRecordSetData(ctx, zoneID, subDomain, info.Value) if err != nil { return fmt.Errorf("yandexcloud: %w", err) } @@ -147,7 +141,7 @@ func (d *DNSProvider) Present(domain, _, keyAuth string) error { } // CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { +func (r *DNSProvider) CleanUp(domain, _, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) @@ -157,7 +151,7 @@ func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { ctx := context.Background() - zones, err := d.getZones(ctx) + zones, err := r.getZones(ctx) if err != nil { return fmt.Errorf("yandexcloud: %w", err) } @@ -179,7 +173,7 @@ func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { return fmt.Errorf("yandexcloud: %w", err) } - err = d.removeRecordSetData(ctx, zoneID, subDomain, info.Value) + err = r.removeRecordSetData(ctx, zoneID, subDomain, info.Value) if err != nil { return fmt.Errorf("yandexcloud: %w", err) } @@ -189,17 +183,17 @@ func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval +func (r *DNSProvider) Timeout() (timeout, interval time.Duration) { + return r.config.PropagationTimeout, r.config.PollingInterval } // getZones retrieves available zones from yandex cloud. -func (d *DNSProvider) getZones(ctx context.Context) ([]*ycdnsproto.DnsZone, error) { - list := &ycdnsproto.ListDnsZonesRequest{ - FolderId: d.config.FolderID, +func (r *DNSProvider) getZones(ctx context.Context) ([]*ycdns.DnsZone, error) { + list := &ycdns.ListDnsZonesRequest{ + FolderId: r.config.FolderID, } - response, err := d.client.List(ctx, list) + response, err := r.client.DNS().DnsZone().List(ctx, list) if err != nil { return nil, errors.New("unable to fetch dns zones") } @@ -207,29 +201,28 @@ func (d *DNSProvider) getZones(ctx context.Context) ([]*ycdnsproto.DnsZone, erro return response.GetDnsZones(), nil } -func (d *DNSProvider) upsertRecordSetData(ctx context.Context, zoneID, name, value string) error { - get := &ycdnsproto.GetDnsZoneRecordSetRequest{ +func (r *DNSProvider) upsertRecordSetData(ctx context.Context, zoneID, name, value string) error { + get := &ycdns.GetDnsZoneRecordSetRequest{ DnsZoneId: zoneID, Name: name, Type: "TXT", } - exist, err := d.client.GetRecordSet(ctx, get) + exist, err := r.client.DNS().DnsZone().GetRecordSet(ctx, get) if err != nil { if !strings.Contains(err.Error(), "RecordSet not found") { return err } } - record := &ycdnsproto.RecordSet{ + record := &ycdns.RecordSet{ Name: name, Type: "TXT", - Ttl: int64(d.config.TTL), + Ttl: int64(r.config.TTL), Data: []string{}, } - var deletions []*ycdnsproto.RecordSet - + var deletions []*ycdns.RecordSet if exist != nil { record.SetData(append(record.GetData(), exist.GetData()...)) deletions = append(deletions, exist) @@ -241,25 +234,25 @@ func (d *DNSProvider) upsertRecordSetData(ctx context.Context, zoneID, name, val return nil } - update := &ycdnsproto.UpdateRecordSetsRequest{ + update := &ycdns.UpdateRecordSetsRequest{ DnsZoneId: zoneID, Deletions: deletions, - Additions: []*ycdnsproto.RecordSet{record}, + Additions: []*ycdns.RecordSet{record}, } - _, err = d.client.UpdateRecordSets(ctx, update) + _, err = r.client.DNS().DnsZone().UpdateRecordSets(ctx, update) return err } -func (d *DNSProvider) removeRecordSetData(ctx context.Context, zoneID, name, value string) error { - get := &ycdnsproto.GetDnsZoneRecordSetRequest{ +func (r *DNSProvider) removeRecordSetData(ctx context.Context, zoneID, name, value string) error { + get := &ycdns.GetDnsZoneRecordSetRequest{ DnsZoneId: zoneID, Name: name, Type: "TXT", } - previousRecord, err := d.client.GetRecordSet(ctx, get) + previousRecord, err := r.client.DNS().DnsZone().GetRecordSet(ctx, get) if err != nil { if strings.Contains(err.Error(), "RecordSet not found") { // RecordSet is not present, nothing to do @@ -269,14 +262,14 @@ func (d *DNSProvider) removeRecordSetData(ctx context.Context, zoneID, name, val return err } - var additions []*ycdnsproto.RecordSet + var additions []*ycdns.RecordSet if len(previousRecord.GetData()) > 1 { // RecordSet is not empty we should update it - record := &ycdnsproto.RecordSet{ + record := &ycdns.RecordSet{ Name: name, Type: "TXT", - Ttl: int64(d.config.TTL), + Ttl: int64(r.config.TTL), Data: []string{}, } @@ -289,35 +282,34 @@ func (d *DNSProvider) removeRecordSetData(ctx context.Context, zoneID, name, val additions = append(additions, record) } - update := &ycdnsproto.UpdateRecordSetsRequest{ + update := &ycdns.UpdateRecordSetsRequest{ DnsZoneId: zoneID, - Deletions: []*ycdnsproto.RecordSet{previousRecord}, + Deletions: []*ycdns.RecordSet{previousRecord}, Additions: additions, } - _, err = d.client.UpdateRecordSets(ctx, update) + _, err = r.client.DNS().DnsZone().UpdateRecordSets(ctx, update) return err } // decodeCredentials converts base64 encoded json of iam token to struct. -func decodeCredentials(accountB64 string) (credentials.Credentials, error) { +func decodeCredentials(accountB64 string) (ycsdk.Credentials, error) { account, err := base64.StdEncoding.DecodeString(accountB64) if err != nil { return nil, err } key := &iamkey.Key{} - err = json.Unmarshal(account, key) if err != nil { return nil, err } - return credentials.ServiceAccountKey(key) + return ycsdk.ServiceAccountKey(key) } -func appendRecordSetData(record *ycdnsproto.RecordSet, value string) bool { +func appendRecordSetData(record *ycdns.RecordSet, value string) bool { if slices.Contains(record.GetData(), value) { return false } diff --git a/providers/dns/yandexcloud/yandexcloud.toml b/providers/dns/yandexcloud/yandexcloud.toml index d4b40bb1d..97677b996 100644 --- a/providers/dns/yandexcloud/yandexcloud.toml +++ b/providers/dns/yandexcloud/yandexcloud.toml @@ -7,7 +7,7 @@ Since = "v4.9.0" Example = ''' YANDEX_CLOUD_IAM_TOKEN= \ YANDEX_CLOUD_FOLDER_ID= \ -lego --dns yandexcloud -d '*.example.com' -d example.com run +lego --email you@example.com --dns yandexcloud --domains "example.org" --domains "*.example.org" run # --- @@ -20,7 +20,7 @@ YANDEX_CLOUD_IAM_TOKEN=$(echo '{ \ "private_key": "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----" \ }' | base64) \ YANDEX_CLOUD_FOLDER_ID= \ -lego --dns yandexcloud -d '*.example.com' -d example.com run +lego --email you@example.com --dns yandexcloud --domains "example.org" --domains "*.example.org" run ''' Additional = ''' @@ -40,9 +40,9 @@ cat key.json | base64 YANDEX_CLOUD_IAM_TOKEN = "The base64 encoded json which contains information about iam token of service account with `dns.admin` permissions" YANDEX_CLOUD_FOLDER_ID = "The string id of folder (aka project) in Yandex Cloud" [Configuration.Additional] - YANDEX_CLOUD_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - YANDEX_CLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - YANDEX_CLOUD_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)" + YANDEX_CLOUD_POLLING_INTERVAL = "Time between DNS propagation check" + YANDEX_CLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + YANDEX_CLOUD_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://cloud.yandex.com/en/docs/dns/quickstart" diff --git a/providers/dns/yandexcloud/yandexcloud_test.go b/providers/dns/yandexcloud/yandexcloud_test.go index 52dad574d..48f75d134 100644 --- a/providers/dns/yandexcloud/yandexcloud_test.go +++ b/providers/dns/yandexcloud/yandexcloud_test.go @@ -71,7 +71,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -144,7 +143,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -158,7 +156,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/zoneedit/internal/client.go b/providers/dns/zoneedit/internal/client.go deleted file mode 100644 index c8b99e173..000000000 --- a/providers/dns/zoneedit/internal/client.go +++ /dev/null @@ -1,108 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/xml" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "slices" - "time" - - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" -) - -const defaultBaseURL = "https://dynamic.zoneedit.com" - -// Client the ZoneEdit API client. -type Client struct { - user string - authToken string - - baseURL *url.URL - HTTPClient *http.Client -} - -// NewClient creates a new Client. -func NewClient(user, authToken string) (*Client, error) { - if user == "" || authToken == "" { - return nil, errors.New("credentials missing") - } - - baseURL, _ := url.Parse(defaultBaseURL) - - return &Client{ - user: user, - authToken: authToken, - baseURL: baseURL, - HTTPClient: &http.Client{Timeout: 10 * time.Second}, - }, nil -} - -func (c *Client) CreateTXTRecord(domain, rdata string) error { - return c.perform("txt-create.php", domain, rdata) -} - -func (c *Client) DeleteTXTRecord(domain, rdata string) error { - return c.perform("txt-delete.php", domain, rdata) -} - -func (c *Client) perform(actionPath, domain, rdata string) error { - endpoint := c.baseURL.JoinPath(actionPath) - - query := endpoint.Query() - query.Set("host", domain) - query.Set("rdata", rdata) - endpoint.RawQuery = query.Encode() - - req, err := http.NewRequest(http.MethodGet, endpoint.String(), http.NoBody) - if err != nil { - return err - } - - return c.do(req) -} - -func (c *Client) do(req *http.Request) error { - req.SetBasicAuth(c.user, c.authToken) - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return errutils.NewHTTPDoError(req, err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode/100 != 2 { - raw, _ := io.ReadAll(resp.Body) - - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return errutils.NewReadResponseError(req, resp.StatusCode, err) - } - - if bytes.Contains(raw, []byte("SUCCESS CODE")) { - return nil - } - - raw = bytes.TrimSpace(raw) - - // The answer is not an XML valid (missing closing), so I fix it to parse it. - if bytes.HasSuffix(raw, []byte(">")) { - raw = slices.Concat(raw[:len(raw)-1], []byte("/>")) - } - - var apiErr APIError - - err = xml.Unmarshal(raw, &apiErr) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) - } - - return fmt.Errorf("[status code: %d] %w", resp.StatusCode, apiErr) -} diff --git a/providers/dns/zoneedit/internal/client_test.go b/providers/dns/zoneedit/internal/client_test.go deleted file mode 100644 index 1d9f9be79..000000000 --- a/providers/dns/zoneedit/internal/client_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package internal - -import ( - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-acme/lego/v4/platform/tester/servermock" - "github.com/stretchr/testify/require" -) - -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder(func(server *httptest.Server) (*Client, error) { - client, err := NewClient("user", "secret") - if err != nil { - return nil, err - } - - client.baseURL, _ = url.Parse(server.URL) - client.HTTPClient = server.Client() - - return client, nil - }) -} - -func TestClient_CreateTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("GET /txt-create.php", - servermock.ResponseFromFixture("success.xml")). - Build(t) - - err := client.CreateTXTRecord("_acme-challenge.example.com", "value") - require.NoError(t, err) -} - -func TestClient_CreateTXTRecord_error(t *testing.T) { - client := mockBuilder(). - Route("GET /txt-create.php", - servermock.ResponseFromFixture("error.xml")). - Build(t) - - err := client.CreateTXTRecord("_acme-challenge.example.com", "value") - require.EqualError(t, err, "[status code: 200] 708: Failed Login: user (_acme-challenge.example.com)") -} - -func TestClient_DeleteTXTRecord(t *testing.T) { - client := mockBuilder(). - Route("GET /txt-delete.php", - servermock.ResponseFromFixture("success.xml")). - Build(t) - - err := client.DeleteTXTRecord("_acme-challenge.example.com", "value") - require.NoError(t, err) -} - -func TestClient_DeleteTXTRecord_error(t *testing.T) { - client := mockBuilder(). - Route("GET /txt-delete.php", - servermock.ResponseFromFixture("error.xml")). - Build(t) - - err := client.DeleteTXTRecord("_acme-challenge.example.com", "value") - require.EqualError(t, err, "[status code: 200] 708: Failed Login: user (_acme-challenge.example.com)") -} diff --git a/providers/dns/zoneedit/internal/fixtures/error.xml b/providers/dns/zoneedit/internal/fixtures/error.xml deleted file mode 100644 index 6c0f1de60..000000000 --- a/providers/dns/zoneedit/internal/fixtures/error.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/providers/dns/zoneedit/internal/fixtures/success.xml b/providers/dns/zoneedit/internal/fixtures/success.xml deleted file mode 100644 index 80d75169d..000000000 --- a/providers/dns/zoneedit/internal/fixtures/success.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/providers/dns/zoneedit/internal/types.go b/providers/dns/zoneedit/internal/types.go deleted file mode 100644 index 96fa41c36..000000000 --- a/providers/dns/zoneedit/internal/types.go +++ /dev/null @@ -1,18 +0,0 @@ -package internal - -import ( - "encoding/xml" - "fmt" -) - -type APIError struct { - XMLName xml.Name `xml:"ERROR"` - Text string `xml:",chardata"` - Code string `xml:"CODE,attr"` - Message string `xml:"TEXT,attr"` - Zone string `xml:"ZONE,attr"` -} - -func (a APIError) Error() string { - return fmt.Sprintf("%s: %s (%s)", a.Code, a.Message, a.Zone) -} diff --git a/providers/dns/zoneedit/zoneedit.go b/providers/dns/zoneedit/zoneedit.go deleted file mode 100644 index c815f975a..000000000 --- a/providers/dns/zoneedit/zoneedit.go +++ /dev/null @@ -1,126 +0,0 @@ -// Package zoneedit implements a DNS provider for solving the DNS-01 challenge using ZoneEdit. -package zoneedit - -import ( - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" - "github.com/go-acme/lego/v4/providers/dns/zoneedit/internal" -) - -// Environment variables names. -const ( - envNamespace = "ZONEEDIT_" - - EnvUser = envNamespace + "USER" - EnAuthToken = envNamespace + "AUTH_TOKEN" - - EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" - EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" -) - -// Config is used to configure the creation of the DNSProvider. -type Config struct { - User string - AuthToken string - - PropagationTimeout time.Duration - PollingInterval time.Duration - HTTPClient *http.Client -} - -// NewDefaultConfig returns a default configuration for the DNSProvider. -func NewDefaultConfig() *Config { - return &Config{ - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, - } -} - -// DNSProvider implements the challenge.Provider interface. -type DNSProvider struct { - config *Config - client *internal.Client -} - -// NewDNSProvider returns a DNSProvider instance configured for ZoneEdit. -func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvUser, EnAuthToken) - if err != nil { - return nil, fmt.Errorf("zoneedit: %w", err) - } - - config := NewDefaultConfig() - config.User = values[EnvUser] - config.AuthToken = values[EnAuthToken] - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for ZoneEdit. -func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { - if config == nil { - return nil, errors.New("zoneedit: the configuration of the DNS provider is nil") - } - - client, err := internal.NewClient(config.User, config.AuthToken) - if err != nil { - return nil, fmt.Errorf("zoneedit: %w", err) - } - - if config.HTTPClient != nil { - client.HTTPClient = config.HTTPClient - } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - - return &DNSProvider{ - config: config, - client: client, - }, nil -} - -// Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - err := d.client.CreateTXTRecord(dns01.UnFqdn(info.EffectiveFQDN), info.Value) - if err != nil { - return fmt.Errorf("zoneedit: create TXT record: %w", err) - } - - // ERROR CODE="702" TEXT="Minimum 10 seconds between requests" - time.Sleep(11 * time.Second) - - return nil -} - -// CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - info := dns01.GetChallengeInfo(domain, keyAuth) - - err := d.client.DeleteTXTRecord(dns01.UnFqdn(info.EffectiveFQDN), info.Value) - if err != nil { - return fmt.Errorf("zoneedit: delete TXT record: %w", err) - } - - // ERROR CODE="702" TEXT="Minimum 10 seconds between requests" - time.Sleep(11 * time.Second) - - return nil -} - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.config.PropagationTimeout, d.config.PollingInterval -} diff --git a/providers/dns/zoneedit/zoneedit.toml b/providers/dns/zoneedit/zoneedit.toml deleted file mode 100644 index cdc53b33a..000000000 --- a/providers/dns/zoneedit/zoneedit.toml +++ /dev/null @@ -1,23 +0,0 @@ -Name = "ZoneEdit" -Description = '''''' -URL = "https://www.zoneedit.com" -Code = "zoneedit" -Since = "v4.25.0" - -Example = ''' -ZONEEDIT_USER="xxxxxxxxxxxxxxxxxxxxx" \ -ZONEEDIT_AUTH_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns zoneedit -d '*.example.com' -d example.com run -''' - -[Configuration] - [Configuration.Credentials] - ZONEEDIT_USER = "User ID" - ZONEEDIT_AUTH_TOKEN = "Authentication token" - [Configuration.Additional] - ZONEEDIT_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - ZONEEDIT_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - ZONEEDIT_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" - -[Links] - API = "https://support.zoneedit.com/en/knowledgebase/article/changes-to-dynamic-dns" diff --git a/providers/dns/zoneedit/zoneedit_test.go b/providers/dns/zoneedit/zoneedit_test.go deleted file mode 100644 index 0b251fddf..000000000 --- a/providers/dns/zoneedit/zoneedit_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package zoneedit - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest(EnvUser, EnAuthToken).WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvUser: "user", - EnAuthToken: "secret", - }, - }, - { - desc: "missing user ID", - envVars: map[string]string{ - EnvUser: "", - EnAuthToken: "secret", - }, - expected: "zoneedit: some credentials information are missing: ZONEEDIT_USER", - }, - { - desc: "missing auth token", - envVars: map[string]string{ - EnvUser: "user", - EnAuthToken: "", - }, - expected: "zoneedit: some credentials information are missing: ZONEEDIT_AUTH_TOKEN", - }, - { - desc: "missing credentials", - envVars: map[string]string{}, - expected: "zoneedit: some credentials information are missing: ZONEEDIT_USER,ZONEEDIT_AUTH_TOKEN", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - defer envTest.RestoreEnv() - - envTest.ClearEnv() - - envTest.Apply(test.envVars) - - p, err := NewDNSProvider() - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestNewDNSProviderConfig(t *testing.T) { - testCases := []struct { - desc string - user string - authToken string - expected string - }{ - { - desc: "success", - user: "user", - authToken: "secret", - }, - { - desc: "missing user ID", - authToken: "secret", - expected: "zoneedit: credentials missing", - }, - { - desc: "missing auth token", - user: "user", - expected: "zoneedit: credentials missing", - }, - { - desc: "missing credentials", - expected: "zoneedit: credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.User = test.user - config.AuthToken = test.authToken - - p, err := NewDNSProviderConfig(config) - - if test.expected == "" { - require.NoError(t, err) - require.NotNil(t, p) - require.NotNil(t, p.config) - require.NotNil(t, p.client) - } else { - require.EqualError(t, err, test.expected) - } - }) - } -} - -func TestLivePresent(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.Present(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} - -func TestLiveCleanUp(t *testing.T) { - if !envTest.IsLiveTest() { - t.Skip("skipping live test") - } - - envTest.RestoreEnv() - - provider, err := NewDNSProvider() - require.NoError(t, err) - - err = provider.CleanUp(envTest.GetDomain(), "", "123d==") - require.NoError(t, err) -} diff --git a/providers/dns/zoneee/internal/client.go b/providers/dns/zoneee/internal/client.go index 9446cd771..e4463b83e 100644 --- a/providers/dns/zoneee/internal/client.go +++ b/providers/dns/zoneee/internal/client.go @@ -26,7 +26,7 @@ type Client struct { } // NewClient creates a new Client. -func NewClient(username, apiKey string) *Client { +func NewClient(username string, apiKey string) *Client { baseURL, _ := url.Parse(DefaultEndpoint) return &Client{ diff --git a/providers/dns/zoneee/internal/client_test.go b/providers/dns/zoneee/internal/client_test.go index c2f0e781e..9e53117ac 100644 --- a/providers/dns/zoneee/internal/client_test.go +++ b/providers/dns/zoneee/internal/client_test.go @@ -1,36 +1,65 @@ package internal import ( + "context" + "fmt" + "io" "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func mockBuilder() *servermock.Builder[*Client] { - return servermock.NewBuilder[*Client]( - func(server *httptest.Server) (*Client, error) { - client := NewClient("user", "secret") - client.HTTPClient = server.Client() - client.BaseURL, _ = url.Parse(server.URL) +func setupTest(t *testing.T, method, pattern string, status int, file string) *Client { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithBasicAuth("user", "secret"), - ) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + if file == "" { + rw.WriteHeader(status) + return + } + + open, err := os.Open(filepath.Join("fixtures", file)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = open.Close() }() + + rw.WriteHeader(status) + _, err = io.Copy(rw, open) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + client := NewClient("user", "secret") + client.HTTPClient = server.Client() + client.BaseURL, _ = url.Parse(server.URL) + + return client } func TestClient_GetTxtRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/example.com/txt", servermock.ResponseFromFixture("get-txt-records.json")). - Build(t) + client := setupTest(t, http.MethodGet, "/dns/example.com/txt", http.StatusOK, "get-txt-records.json") - records, err := client.GetTxtRecords(t.Context(), "example.com") + records, err := client.GetTxtRecords(context.Background(), "example.com") require.NoError(t, err) expected := []TXTRecord{ @@ -41,14 +70,9 @@ func TestClient_GetTxtRecords(t *testing.T) { } func TestClient_AddTxtRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /dns/example.com/txt", - servermock.ResponseFromFixture("create-txt-record.json"). - WithStatusCode(http.StatusCreated), - servermock.CheckRequestJSONBody(`{"name":"prefix.example.com","destination":"server.example.com"}`)). - Build(t) + client := setupTest(t, http.MethodPost, "/dns/example.com/txt", http.StatusCreated, "create-txt-record.json") - records, err := client.AddTxtRecord(t.Context(), "example.com", TXTRecord{Name: "prefix.example.com", Destination: "server.example.com"}) + records, err := client.AddTxtRecord(context.Background(), "example.com", TXTRecord{Name: "prefix.example.com", Destination: "server.example.com"}) require.NoError(t, err) expected := []TXTRecord{ @@ -59,12 +83,8 @@ func TestClient_AddTxtRecord(t *testing.T) { } func TestClient_RemoveTxtRecord(t *testing.T) { - client := mockBuilder(). - Route("DELETE /dns/example.com/txt/123", - servermock.Noop(). - WithStatusCode(http.StatusNoContent)). - Build(t) + client := setupTest(t, http.MethodDelete, "/dns/example.com/txt/123", http.StatusNoContent, "") - err := client.RemoveTxtRecord(t.Context(), "example.com", "123") + err := client.RemoveTxtRecord(context.Background(), "example.com", "123") require.NoError(t, err) } diff --git a/providers/dns/zoneee/zoneee.go b/providers/dns/zoneee/zoneee.go index 5c34ea1c9..59dd0baf6 100644 --- a/providers/dns/zoneee/zoneee.go +++ b/providers/dns/zoneee/zoneee.go @@ -9,10 +9,8 @@ import ( "net/url" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/go-acme/lego/v4/providers/dns/zoneee/internal" ) @@ -29,8 +27,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. type Config struct { Endpoint *url.URL @@ -70,7 +66,6 @@ func NewDNSProvider() (*DNSProvider, error) { } rawEndpoint := env.GetOrDefaultString(EnvEndpoint, internal.DefaultEndpoint) - endpoint, err := url.Parse(rawEndpoint) if err != nil { return nil, fmt.Errorf("zoneee: %w", err) @@ -107,9 +102,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } - - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - if config.Endpoint != nil { client.BaseURL = config.Endpoint } @@ -127,6 +119,11 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) + record := internal.TXTRecord{ + Name: dns01.UnFqdn(info.EffectiveFQDN), + Destination: info.Value, + } + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { return fmt.Errorf("zoneee: could not find zone for domain %q: %w", domain, err) @@ -134,16 +131,10 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { authZone = dns01.UnFqdn(authZone) - record := internal.TXTRecord{ - Name: dns01.UnFqdn(info.EffectiveFQDN), - Destination: info.Value, - } - _, err = d.client.AddTxtRecord(context.Background(), authZone, record) if err != nil { return fmt.Errorf("zoneee: %w", err) } - return nil } @@ -166,7 +157,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } var id string - for _, record := range records { if record.Destination == info.Value { id = record.ID diff --git a/providers/dns/zoneee/zoneee.toml b/providers/dns/zoneee/zoneee.toml index ab7133180..16704671f 100644 --- a/providers/dns/zoneee/zoneee.toml +++ b/providers/dns/zoneee/zoneee.toml @@ -7,7 +7,7 @@ Since = "v2.1.0" Example = ''' ZONEEE_API_USER=xxxxx \ ZONEEE_API_KEY=yyyyy \ -lego --dns zoneee -d '*.example.com' -d example.com run +lego --email you@example.com --dns zoneee --domains my.example.org run ''' [Configuration] @@ -16,9 +16,10 @@ lego --dns zoneee -d '*.example.com' -d example.com run ZONEEE_API_KEY = "API key" [Configuration.Additional] ZONEEE_ENDPOINT = "API endpoint URL" - ZONEEE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 5)" - ZONEEE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 300)" - ZONEEE_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + ZONEEE_POLLING_INTERVAL = "Time between DNS propagation check" + ZONEEE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + ZONEEE_TTL = "The TTL of the TXT record used for the DNS challenge" + ZONEEE_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://api.zone.eu/v2" diff --git a/providers/dns/zoneee/zoneee_test.go b/providers/dns/zoneee/zoneee_test.go index 9ad87c02a..1f2909fa7 100644 --- a/providers/dns/zoneee/zoneee_test.go +++ b/providers/dns/zoneee/zoneee_test.go @@ -6,22 +6,17 @@ import ( "net/http" "net/http/httptest" "net/url" + "path" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/go-acme/lego/v4/providers/dns/zoneee/internal" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" -const ( - fakeUsername = "user" - fakeAPIKey = "secret" -) - var envTest = tester.NewEnvTest(EnvEndpoint, EnvAPIUser, EnvAPIKey). WithLiveTestRequirements(EnvAPIUser, EnvAPIKey). WithDomain(envDomain) @@ -77,7 +72,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -100,6 +94,7 @@ func TestNewDNSProviderConfig(t *testing.T) { desc string apiUser string apiKey string + endpoint string expected string }{ { @@ -129,6 +124,10 @@ func TestNewDNSProviderConfig(t *testing.T) { config.APIKey = test.apiKey config.Username = test.apiUser + if test.endpoint != "" { + config.Endpoint = mustParse(test.endpoint) + } + p, err := NewDNSProviderConfig(config) if test.expected == "" { @@ -148,33 +147,57 @@ func TestDNSProvider_Present(t *testing.T) { testCases := []struct { desc string - builder *servermock.Builder[*DNSProvider] + username string + apiKey string + handlers map[string]http.HandlerFunc expectedError string }{ { - desc: "success", - builder: mockBuilder(fakeUsername, fakeAPIKey). - Route("POST /dns/"+hostedZone+"/txt", - mockHandlerCreateRecord()), + desc: "success", + username: "bar", + apiKey: "foo", + handlers: map[string]http.HandlerFunc{ + path.Join("/", "dns", hostedZone, "txt"): mockHandlerCreateRecord, + }, }, { - desc: "invalid auth", - builder: mockBuilder("nope", "nope"). - Route("POST /dns/"+hostedZone+"/txt", nil), + desc: "invalid auth", + username: "nope", + apiKey: "foo", + handlers: map[string]http.HandlerFunc{ + path.Join("/", "dns", hostedZone, "txt"): mockHandlerCreateRecord, + }, expectedError: "zoneee: unexpected status code: [status code: 401] body: Unauthorized", }, { desc: "error", - builder: mockBuilder(fakeUsername, fakeAPIKey), + username: "bar", + apiKey: "foo", expectedError: "zoneee: unexpected status code: [status code: 404] body: 404 page not found", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - provider := test.builder.Build(t) + t.Parallel() - err := provider.Present(domain, "token", "key") + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + for uri, handler := range test.handlers { + mux.HandleFunc(uri, handler) + } + + config := NewDefaultConfig() + config.Endpoint = mustParse(server.URL) + config.Username = test.username + config.APIKey = test.apiKey + + p, err := NewDNSProviderConfig(config) + require.NoError(t, err) + + err = p.Present(domain, "token", "key") if test.expectedError == "" { require.NoError(t, err) } else { @@ -190,49 +213,81 @@ func TestDNSProvider_Cleanup(t *testing.T) { testCases := []struct { desc string - builder *servermock.Builder[*DNSProvider] + username string + apiKey string + handlers map[string]http.HandlerFunc expectedError string }{ { - desc: "success", - builder: mockBuilder(fakeUsername, fakeAPIKey). - Route("GET /dns/"+hostedZone+"/txt", - mockHandlerGetRecords([]internal.TXTRecord{{ - ID: "1234", - Name: domain, - Destination: "LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM", - Delete: true, - Modify: true, - }})). - Route("DELETE /dns/"+hostedZone+"/txt/1234", - servermock.Noop(). - WithStatusCode(http.StatusNoContent)), + desc: "success", + username: "bar", + apiKey: "foo", + handlers: map[string]http.HandlerFunc{ + path.Join("/", "dns", hostedZone, "txt"): mockHandlerGetRecords([]internal.TXTRecord{{ + ID: "1234", + Name: domain, + Destination: "LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM", + Delete: true, + Modify: true, + }}), + path.Join("/", "dns", hostedZone, "txt", "1234"): mockHandlerDeleteRecord, + }, }, { - desc: "no txt records", - builder: mockBuilder(fakeUsername, fakeAPIKey). - Route("GET /dns/"+hostedZone+"/txt", - mockHandlerGetRecords([]internal.TXTRecord{})), + desc: "no txt records", + username: "bar", + apiKey: "foo", + handlers: map[string]http.HandlerFunc{ + path.Join("/", "dns", hostedZone, "txt"): mockHandlerGetRecords([]internal.TXTRecord{}), + path.Join("/", "dns", hostedZone, "txt", "1234"): mockHandlerDeleteRecord, + }, expectedError: "zoneee: txt record does not exist for LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM", }, { - desc: "invalid auth", - builder: mockBuilder("nope", "nope"). - Route("GET /dns/"+hostedZone+"/txt", nil), + desc: "invalid auth", + username: "nope", + apiKey: "foo", + handlers: map[string]http.HandlerFunc{ + path.Join("/", "dns", hostedZone, "txt"): mockHandlerGetRecords([]internal.TXTRecord{{ + ID: "1234", + Name: domain, + Destination: "LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM", + Delete: true, + Modify: true, + }}), + path.Join("/", "dns", hostedZone, "txt", "1234"): mockHandlerDeleteRecord, + }, expectedError: "zoneee: unexpected status code: [status code: 401] body: Unauthorized", }, { desc: "error", - builder: mockBuilder(fakeUsername, fakeAPIKey), + username: "bar", + apiKey: "foo", expectedError: "zoneee: unexpected status code: [status code: 404] body: 404 page not found", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - provider := test.builder.Build(t) + t.Parallel() - err := provider.CleanUp(domain, "token", "key") + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + for uri, handler := range test.handlers { + mux.HandleFunc(uri, handler) + } + + config := NewDefaultConfig() + config.Endpoint = mustParse(server.URL) + config.Username = test.username + config.APIKey = test.apiKey + + p, err := NewDNSProviderConfig(config) + require.NoError(t, err) + + err = p.CleanUp(domain, "token", "key") if test.expectedError == "" { require.NoError(t, err) } else { @@ -248,7 +303,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -262,7 +316,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -272,59 +325,72 @@ func TestLiveCleanUp(t *testing.T) { require.NoError(t, err) } -func mockBuilder(username, apiKey string) *servermock.Builder[*DNSProvider] { - return servermock.NewBuilder( - func(server *httptest.Server) (*DNSProvider, error) { - config := NewDefaultConfig() - config.HTTPClient = server.Client() - config.Endpoint, _ = url.Parse(server.URL) - config.Username = username - config.APIKey = apiKey - - return NewDNSProviderConfig(config) - }, - checkBasicAuth()) +func mustParse(rawURL string) *url.URL { + uri, err := url.Parse(rawURL) + if err != nil { + panic(err) + } + return uri } -func mockHandlerCreateRecord() http.HandlerFunc { - return encodeJSONHandler(func(req *http.Request, rw http.ResponseWriter) (any, error) { - record := internal.TXTRecord{} +func mockHandlerCreateRecord(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } - err := json.NewDecoder(req.Body).Decode(&record) - if err != nil { - return nil, err - } + username, apiKey, ok := req.BasicAuth() + if username != "bar" || apiKey != "foo" || !ok { + rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q`, "Please enter your username and API key.")) + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } - record.ID = "1234" - record.Delete = true - record.Modify = true - record.ResourceURL = req.URL.String() + "/1234" + record := internal.TXTRecord{} + err := json.NewDecoder(req.Body).Decode(&record) + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } - return []internal.TXTRecord{record}, nil - }) + record.ID = "1234" + record.Delete = true + record.Modify = true + record.ResourceURL = req.URL.String() + "/1234" + + bytes, err := json.Marshal([]internal.TXTRecord{record}) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + if _, err = rw.Write(bytes); err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } } func mockHandlerGetRecords(records []internal.TXTRecord) http.HandlerFunc { - return encodeJSONHandler(func(req *http.Request, rw http.ResponseWriter) (any, error) { - for _, record := range records { - if record.ResourceURL == "" { - record.ResourceURL = req.URL.String() + "/" + record.ID - } - } - - return records, nil - }) -} - -func encodeJSONHandler(build func(req *http.Request, rw http.ResponseWriter) (any, error)) http.HandlerFunc { return func(rw http.ResponseWriter, req *http.Request) { - data, err := build(req, rw) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) + if req.Method != http.MethodGet { + http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } - bytes, err := json.Marshal(data) + username, apiKey, ok := req.BasicAuth() + if username != "bar" || apiKey != "foo" || !ok { + rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q`, "Please enter your username and API key.")) + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + for _, value := range records { + if value.ResourceURL == "" { + value.ResourceURL = req.URL.String() + "/" + value.ID + } + } + + bytes, err := json.Marshal(records) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return @@ -337,18 +403,18 @@ func encodeJSONHandler(build func(req *http.Request, rw http.ResponseWriter) (an } } -func checkBasicAuth() servermock.LinkFunc { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - username, apiKey, ok := req.BasicAuth() - if username != fakeUsername || apiKey != fakeAPIKey || !ok { - rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q`, "Please enter your username and API key.")) - http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - - return - } - - next.ServeHTTP(rw, req) - }) +func mockHandlerDeleteRecord(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodDelete { + http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return } + + username, apiKey, ok := req.BasicAuth() + if username != "bar" || apiKey != "foo" || !ok { + rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q`, "Please enter your username and API key.")) + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + rw.WriteHeader(http.StatusNoContent) } diff --git a/providers/dns/zonomi/zonomi.go b/providers/dns/zonomi/zonomi.go index fe54b80fc..5d1a2c792 100644 --- a/providers/dns/zonomi/zonomi.go +++ b/providers/dns/zonomi/zonomi.go @@ -2,12 +2,12 @@ package zonomi import ( + "context" "errors" "fmt" "net/http" "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/internal/rimuhosting" @@ -25,17 +25,20 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const defaultBaseURL = "https://zonomi.com/app/dns/dyndns.jsp" - -var _ challenge.ProviderTimeout = (*DNSProvider)(nil) - // Config is used to configure the creation of the DNSProvider. -type Config = rimuhosting.Config +type Config struct { + APIKey string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt(EnvTTL, rimuhosting.DefaultTTL), + TTL: env.GetOrDefaultInt(EnvTTL, 3600), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ @@ -46,7 +49,8 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - prv challenge.ProviderTimeout + config *Config + client *rimuhosting.Client } // NewDNSProvider returns a DNSProvider instance configured for Zonomi. @@ -69,19 +73,48 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("zonomi: the configuration of the DNS provider is nil") } - provider, err := rimuhosting.NewDNSProviderConfig(config, defaultBaseURL) - if err != nil { - return nil, fmt.Errorf("zonomi: %w", err) + if config.APIKey == "" { + return nil, errors.New("zonomi: incomplete credentials, missing API key") } - return &DNSProvider{prv: provider}, nil + client := rimuhosting.NewClient(config.APIKey) + client.BaseURL = rimuhosting.DefaultZonomiBaseURL + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + return &DNSProvider{config: config, client: client}, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - err := d.prv.Present(domain, token, keyAuth) + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + records, err := d.client.FindTXTRecords(ctx, dns01.UnFqdn(info.EffectiveFQDN)) if err != nil { - return fmt.Errorf("zonomi: %w", err) + return fmt.Errorf("zonomi: failed to find record(s) for %s: %w", domain, err) + } + + actions := []rimuhosting.ActionParameter{ + rimuhosting.NewAddRecordAction(dns01.UnFqdn(info.EffectiveFQDN), info.Value, d.config.TTL), + } + + for _, record := range records { + actions = append(actions, rimuhosting.NewAddRecordAction(record.Name, record.Content, d.config.TTL)) + } + + _, err = d.client.DoActions(ctx, actions...) + if err != nil { + return fmt.Errorf("zonomi: failed to add record(s) for %s: %w", domain, err) } return nil @@ -89,16 +122,14 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - err := d.prv.CleanUp(domain, token, keyAuth) + info := dns01.GetChallengeInfo(domain, keyAuth) + + action := rimuhosting.NewDeleteRecordAction(dns01.UnFqdn(info.EffectiveFQDN), info.Value) + + _, err := d.client.DoActions(context.Background(), action) if err != nil { - return fmt.Errorf("zonomi: %w", err) + return fmt.Errorf("zonomi: failed to delete record for %s: %w", domain, err) } return nil } - -// Timeout returns the timeout and interval to use when checking for DNS propagation. -// Adjusting here to cope with spikes in propagation times. -func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { - return d.prv.Timeout() -} diff --git a/providers/dns/zonomi/zonomi.toml b/providers/dns/zonomi/zonomi.toml index b91bcaac6..2d3f3e3aa 100644 --- a/providers/dns/zonomi/zonomi.toml +++ b/providers/dns/zonomi/zonomi.toml @@ -6,17 +6,17 @@ Since = "v3.5.0" Example = ''' ZONOMI_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ -lego --dns zonomi -d '*.example.com' -d example.com run +lego --email you@example.com --dns zonomi --domains my.example.org run ''' [Configuration] [Configuration.Credentials] ZONOMI_API_KEY = "User API key" [Configuration.Additional] - ZONOMI_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" - ZONOMI_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" - ZONOMI_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)" - ZONOMI_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)" + ZONOMI_POLLING_INTERVAL = "Time between DNS propagation check" + ZONOMI_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + ZONOMI_TTL = "The TTL of the TXT record used for the DNS challenge" + ZONOMI_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://zonomi.com/app/dns/dyndns.jsp" diff --git a/providers/dns/zonomi/zonomi_test.go b/providers/dns/zonomi/zonomi_test.go index 2e13e937e..fb1b68773 100644 --- a/providers/dns/zonomi/zonomi_test.go +++ b/providers/dns/zonomi/zonomi_test.go @@ -36,7 +36,6 @@ func TestNewDNSProvider(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() envTest.Apply(test.envVars) @@ -46,7 +45,7 @@ func TestNewDNSProvider(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - require.NotNil(t, p.prv) + require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } @@ -84,7 +83,7 @@ func TestNewDNSProviderConfig(t *testing.T) { if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) - require.NotNil(t, p.prv) + require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } @@ -98,7 +97,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -112,7 +110,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/http/memcached/memcached.go b/providers/http/memcached/memcached.go index 376ae8c16..b26def2c4 100644 --- a/providers/http/memcached/memcached.go +++ b/providers/http/memcached/memcached.go @@ -33,14 +33,12 @@ func (w *HTTPProvider) Present(domain, token, keyAuth string) error { var errs []error challengePath := path.Join("/", http01.ChallengePath(token)) - for _, host := range w.hosts { mc, err := memcache.New(host) if err != nil { errs = append(errs, err) continue } - _ = mc.Add(&memcache.Item{ Key: challengePath, Value: []byte(keyAuth), diff --git a/providers/http/memcached/memcached_test.go b/providers/http/memcached/memcached_test.go index 5862efbc6..fb450f988 100644 --- a/providers/http/memcached/memcached_test.go +++ b/providers/http/memcached/memcached_test.go @@ -25,7 +25,6 @@ func loadMemcachedHosts() []string { if memcachedHostsStr != "" { return strings.Split(memcachedHostsStr, ",") } - return nil } @@ -39,7 +38,6 @@ func TestNewMemcachedProviderValid(t *testing.T) { if len(memcachedHosts) == 0 { t.Skip("Skipping memcached tests") } - _, err := NewMemcachedProvider(memcachedHosts) require.NoError(t, err) } @@ -48,7 +46,6 @@ func TestMemcachedPresentSingleHost(t *testing.T) { if len(memcachedHosts) == 0 { t.Skip("Skipping memcached tests") } - p, err := NewMemcachedProvider(memcachedHosts[0:1]) require.NoError(t, err) @@ -67,7 +64,6 @@ func TestMemcachedPresentMultiHost(t *testing.T) { if len(memcachedHosts) <= 1 { t.Skip("Skipping memcached multi-host tests") } - p, err := NewMemcachedProvider(memcachedHosts) require.NoError(t, err) @@ -75,7 +71,6 @@ func TestMemcachedPresentMultiHost(t *testing.T) { err = p.Present(domain, token, keyAuth) require.NoError(t, err) - for _, host := range memcachedHosts { mc, err := memcache.New(host) require.NoError(t, err) @@ -89,7 +84,6 @@ func TestMemcachedPresentPartialFailureMultiHost(t *testing.T) { if len(memcachedHosts) == 0 { t.Skip("Skipping memcached tests") } - hosts := append(memcachedHosts, "5.5.5.5:11211") p, err := NewMemcachedProvider(hosts) require.NoError(t, err) @@ -98,7 +92,6 @@ func TestMemcachedPresentPartialFailureMultiHost(t *testing.T) { err = p.Present(domain, token, keyAuth) require.NoError(t, err) - for _, host := range memcachedHosts { mc, err := memcache.New(host) require.NoError(t, err) @@ -112,7 +105,6 @@ func TestMemcachedCleanup(t *testing.T) { if len(memcachedHosts) == 0 { t.Skip("Skipping memcached tests") } - p, err := NewMemcachedProvider(memcachedHosts) require.NoError(t, err) require.NoError(t, p.CleanUp(domain, token, keyAuth)) diff --git a/providers/http/s3/s3.go b/providers/http/s3/s3.go index e277deeea..07e1eed63 100644 --- a/providers/http/s3/s3.go +++ b/providers/http/s3/s3.go @@ -57,7 +57,6 @@ func (s *HTTPProvider) Present(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("s3: failed to upload token to s3: %w", err) } - return nil } diff --git a/providers/http/webroot/webroot.go b/providers/http/webroot/webroot.go index c94c4579c..c5b49caee 100644 --- a/providers/http/webroot/webroot.go +++ b/providers/http/webroot/webroot.go @@ -29,7 +29,6 @@ func (w *HTTPProvider) Present(domain, token, keyAuth string) error { var err error challengeFilePath := filepath.Join(w.path, http01.ChallengePath(token)) - err = os.MkdirAll(filepath.Dir(challengeFilePath), 0o755) if err != nil { return fmt.Errorf("could not create required directories in webroot for HTTP challenge: %w", err) diff --git a/providers/http/webroot/webroot_test.go b/providers/http/webroot/webroot_test.go index 4c55e2b90..124b324a3 100644 --- a/providers/http/webroot/webroot_test.go +++ b/providers/http/webroot/webroot_test.go @@ -29,7 +29,6 @@ func TestHTTPProvider(t *testing.T) { } var data []byte - data, err = os.ReadFile(challengeFilePath) require.NoError(t, err) diff --git a/registration/registar.go b/registration/registar.go index 5d3ea250b..78e0ce7d8 100644 --- a/registration/registar.go +++ b/registration/registar.go @@ -15,7 +15,7 @@ const mailTo = "mailto:" // of which the client needs to keep track itself. // WARNING: will be removed in the future (acme.ExtendedAccount), https://github.com/go-acme/lego/issues/855. type Resource struct { - Body acme.Account `json:"body"` + Body acme.Account `json:"body,omitempty"` URI string `json:"uri,omitempty"` } @@ -60,7 +60,7 @@ func (r *Registrar) Register(options RegisterOptions) (*Resource, error) { account, err := r.core.Accounts.New(accMsg) if err != nil { // seems impossible - errorDetails := &acme.ProblemDetails{} + var errorDetails acme.ProblemDetails if !errors.As(err, &errorDetails) || errorDetails.HTTPStatus != http.StatusConflict { return nil, err } @@ -84,7 +84,7 @@ func (r *Registrar) RegisterWithExternalAccountBinding(options RegisterEABOption account, err := r.core.Accounts.NewEAB(accMsg, options.Kid, options.HmacEncoded) if err != nil { // seems impossible - errorDetails := &acme.ProblemDetails{} + var errorDetails acme.ProblemDetails if !errors.As(err, &errorDetails) || errorDetails.HTTPStatus != http.StatusConflict { return nil, err } @@ -160,7 +160,6 @@ func (r *Registrar) ResolveAccountByKey() (*Resource, error) { log.Infof("acme: Trying to resolve account by key") accMsg := acme.Account{OnlyReturnExisting: true} - account, err := r.core.Accounts.New(accMsg) if err != nil { return nil, err diff --git a/registration/registar_test.go b/registration/registar_test.go index 43df1d648..efbc4f6f7 100644 --- a/registration/registar_test.go +++ b/registration/registar_test.go @@ -3,30 +3,31 @@ package registration import ( "crypto/rand" "crypto/rsa" - "fmt" "net/http" "testing" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/platform/tester" - "github.com/go-acme/lego/v4/platform/tester/servermock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestRegistrar_ResolveAccountByKey(t *testing.T) { - server := tester.MockACMEServer(). - Route("/account", - http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.Header().Set("Location", - fmt.Sprintf("http://%s/account", req.Context().Value(http.LocalAddrContextKey))) + mux, apiURL := tester.SetupFakeAPI(t) - servermock.JSONEncode(acme.Account{Status: "valid"}).ServeHTTP(rw, req) - })). - BuildHTTPS(t) + mux.HandleFunc("/account", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Location", apiURL+"/account") + err := tester.WriteJSONResponse(w, acme.Account{ + Status: "valid", + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) - key, err := rsa.GenerateKey(rand.Reader, 1024) + key, err := rsa.GenerateKey(rand.Reader, 512) require.NoError(t, err, "Could not generate test key") user := mockUser{ @@ -35,7 +36,7 @@ func TestRegistrar_ResolveAccountByKey(t *testing.T) { privatekey: key, } - core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", key) + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) require.NoError(t, err) registrar := NewRegistrar(core, user)