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..a4d077e5a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -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 @@ -45,8 +44,6 @@ body: - Through Bitnami - Through 1Panel - Through Zoraxy - - Through Certimate - - go install - Other validations: required: true @@ -67,9 +64,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/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 7f6793167..b4e264177 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -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 @@ -24,20 +23,10 @@ body: - 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..274983636 100644 --- a/.github/ISSUE_TEMPLATE/new_dns_provider.yml +++ b/.github/ISSUE_TEMPLATE/new_dns_provider.yml @@ -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 @@ -40,23 +33,10 @@ body: - 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/workflows/documentation.yml b/.github/workflows/documentation.yml index 4f9d444fc..46f7f6730 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.131.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..30ec652a2 100644 --- a/.github/workflows/go-cross.yml +++ b/.github/workflows/go-cross.yml @@ -20,8 +20,13 @@ jobs: 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..d7404a6b8 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.62.0 + HUGO_VERSION: 0.131.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..a102ad796 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 }} @@ -67,21 +64,11 @@ jobs: # https://goreleaser.com/ci/actions/ - name: Run GoReleaser - id: goreleaser uses: goreleaser/goreleaser-action@v6 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..68fd32a68 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,284 +1,269 @@ -version: "2" - -formatters: - enable: - - gci - - gofmt - - gofumpt - - goimports - settings: - gofumpt: - extra-rules: true - gofmt: - rewrite-rules: - - pattern: 'interface{}' - replacement: 'any' - linters: - default: all + enable-all: true disable: - - wsl # Deprecated - - bodyclose - - canonicalheader - - contextcheck - 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) + - 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 + - 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 + - usestdlibvars # false-positive https://github.com/sashamelentyev/usestdlibvars/issues/96 - 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 +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: - - 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 + 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 + +run: + timeout: 10m + +output: + show-stats: true + sort-results: true + sort-order: + - linter + - file issues: + exclude-generated: strict + 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: 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/nameserver_test.go + text: 'findXByFqdnTestCases is a global variable' + linters: + - gochecknoglobals + - path: challenge/http01/domain_matcher.go + text: 'string `Host` has \d occurrences, make it a constant' + linters: + - goconst + - 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/([\d\w]+/)*[\d\w]+_test.go' + text: 'envTest is a global variable' + linters: + - gochecknoglobals + - path: 'providers/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/acmedns/acmedns_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: cmd/zz_gen_cmd_dnshelp.go + linters: + - gocyclo + - funlen + - 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/servercow/internal/types.go + text: 'the methods of "Value" use pointer receiver and non-pointer receiver.' + linters: + - recvcheck + + # Those elements have been replaced by non-exposed structures. + - path: providers/dns/linode/linode_test.go + linters: + - staticcheck + text: "SA1019: linodego\\.(DomainsPagedResponse|DomainRecordsPagedResponse) is deprecated" diff --git a/.goreleaser.yml b/.goreleaser.yml index c358f8a38..c3812ec01 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -42,10 +42,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,54 +51,98 @@ 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 }}" diff --git a/CHANGELOG.md b/CHANGELOG.md index ae73f70f3..bdd07ac56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,365 +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) +## [v4.21.0](https://github.com/go-acme/lego/releases/tag/v4.21.0) (2024-12-20) ### Added @@ -380,17 +21,11 @@ This release contains the same things as v4.23.0. - **[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) +## [v4.20.4](https://github.com/go-acme/lego/releases/tag/v4.20.4) (2024-11-21) 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) +## [v4.20.3](https://github.com/go-acme/lego/releases/tag/v4.20.3) (2024-11-21) ### Fixed @@ -398,10 +33,7 @@ Publish the Snap to the Snapcraft stable channel. - **[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) +## [v4.20.2](https://github.com/go-acme/lego/releases/tag/v4.20.2) (2024-11-11) ### Added @@ -429,41 +61,28 @@ Publish the Snap to the Snapcraft stable channel. - **[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 +## v4.20.1 (2024-11-11) Cancelled due to CI failure. -## v4.20.0 - -- Release date: 2024-11-11 +## v4.20.0 (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) +## [v4.19.2](https://github.com/go-acme/lego/releases/tag/v4.19.2) (2024-10-06) ### 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) +## [v4.19.1](https://github.com/go-acme/lego/releases/tag/v4.19.1) (2024-10-06) ### 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) +## [v4.19.0](https://github.com/go-acme/lego/releases/tag/v4.19.0) (2024-10-03) ### Added @@ -485,10 +104,7 @@ Cancelled due to CI failure. - **[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) +## [v4.18.0](https://github.com/go-acme/lego/releases/tag/v4.18.0) (2024-08-30) ### Added @@ -510,19 +126,13 @@ Cancelled due to CI failure. - **[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) +## [v4.17.4](https://github.com/go-acme/lego/releases/tag/v4.17.4) (2024-06-12) ### 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.3](https://github.com/go-acme/lego/releases/tag/v4.17.3) (2024-05-28) ### Added @@ -550,17 +160,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 +## v4.17.2 (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 +## v4.17.1 (2024-05-28) Canceled due to a release failure related to oci-go-sdk. @@ -569,25 +175,17 @@ The module `github.com/oracle/oci-go-sdk/v65` uses `github.com/gofrs/flock` but Due to that we will remove the Solaris build. -## v4.17.0 - -- Release date: 2024-05-28 +## v4.17.0 (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](https://github.com/go-acme/lego/releases/tag/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](https://github.com/go-acme/lego/releases/tag/v4.16.0) (2024-03-09) ### Added @@ -608,10 +206,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](https://github.com/go-acme/lego/releases/tag/v4.15.0) (2024-01-28) ### Added @@ -649,10 +244,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](https://github.com/go-acme/lego/releases/tag/v4.14.1) (2023-09-20) ### Fixed @@ -660,16 +252,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](https://github.com/go-acme/lego/releases/tag/v4.14.0) (2023-08-20) ### Added @@ -688,29 +275,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](https://github.com/go-acme/lego/releases/tag/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](https://github.com/go-acme/lego/releases/tag/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](https://github.com/go-acme/lego/releases/tag/v4.13.1) (2023-07-20) ### Added @@ -731,35 +309,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](https://github.com/go-acme/lego/releases/tag/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](https://github.com/go-acme/lego/releases/tag/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](https://github.com/go-acme/lego/releases/tag/v4.12.0) (2023-05-28) ### Added @@ -777,10 +344,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](https://github.com/go-acme/lego/releases/tag/v4.11.0) (2023-05-02) ### Added @@ -802,27 +366,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](https://github.com/go-acme/lego/releases/tag/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](https://github.com/go-acme/lego/releases/tag/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](https://github.com/go-acme/lego/releases/tag/v4.10.0) (2023-02-10) ### Added @@ -848,10 +403,7 @@ 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](https://github.com/go-acme/lego/releases/tag/v4.9.1) (2022-11-25) ### Changed @@ -866,10 +418,7 @@ Fix Docker image builds. - **[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](https://github.com/go-acme/lego/releases/tag/v4.9.0) (2022-10-03) ### Added @@ -899,10 +448,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](https://github.com/go-acme/lego/releases/tag/v4.8.0) (2022-06-30) ### Added @@ -918,10 +464,7 @@ Fix Docker image builds. - **[dnsprovider]** hetzner: set min TTL to 60s - **[docs]** refactoring and cleanup -## v4.7.0 - -- Release date: 2022-05-27 -- Tag: [v4.7.0](https://github.com/go-acme/lego/releases/tag/v4.7.0) +## [v4.7.0](https://github.com/go-acme/lego/releases/tag/v4.7.0) (2022-05-27) ### Added @@ -943,10 +486,7 @@ Fix Docker image builds. - **[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](https://github.com/go-acme/lego/releases/tag/v4.6.0) (2022-01-18) ### Added @@ -968,19 +508,13 @@ Fix Docker image builds. - **[dnsprovider]** mythicbeasts: fix token expiration - **[dnsprovider]** rackspace: change zone ID to string -## v4.5.3 - -- Release date: 2021-09-06 -- Tag: [v4.5.3](https://github.com/go-acme/lego/releases/tag/v4.5.3) +## [v4.5.3](https://github.com/go-acme/lego/releases/tag/v4.5.3) (2021-09-06) ### Fixed - **[lib,cli]** fix: missing preferred chain param for renew request -## v4.5.2 - -- Release date: 2021-09-01 -- Tag: [v4.5.2](https://github.com/go-acme/lego/releases/tag/v4.5.2) +## [v4.5.2](https://github.com/go-acme/lego/releases/tag/v4.5.2) (2021-09-01) ### Added @@ -1010,22 +544,15 @@ 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 - -- Release date: 2021-06-08 -- Tag: [v4.4.0](https://github.com/go-acme/lego/releases/tag/v4.4.0) +## [v4.4.0](https://github.com/go-acme/lego/releases/tag/v4.4.0) (2021-06-08) ### Added @@ -1053,19 +580,13 @@ 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 - -- Release date: 2021-03-12 -- Tag: [v4.3.1](https://github.com/go-acme/lego/releases/tag/v4.3.1) +## [v4.3.1](https://github.com/go-acme/lego/releases/tag/v4.3.1) (2021-03-12) ### Fixed - **[dnsprovider]** exoscale: fix dependency version. -## v4.3.0 - -- Release date: 2021-03-10 -- Tag: [v4.3.0](https://github.com/go-acme/lego/releases/tag/v4.3.0) +## [v4.3.0](https://github.com/go-acme/lego/releases/tag/v4.3.0) (2021-03-10) ### Added @@ -1089,10 +610,7 @@ 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 - -- Release date: 2021-01-24 -- Tag: [v4.2.0](https://github.com/go-acme/lego/releases/tag/v4.2.0) +## [v4.2.0](https://github.com/go-acme/lego/releases/tag/v4.2.0) (2021-01-24) ### Added @@ -1112,38 +630,26 @@ Cancelled due to a CI issue, replaced by v4.5.2. - **[dnsprovider]** pdns: URL request creation. - **[lib]** errors: Fix instance not being printed -## v4.1.3 - -- Release date: 2020-11-25 -- Tag: [v4.1.3](https://github.com/go-acme/lego/releases/tag/v4.1.3) +## [v4.1.3](https://github.com/go-acme/lego/releases/tag/v4.1.3) (2020-11-25) ### Fixed - **[dnsprovider]** azure: fix error handling. -## v4.1.2 - -- Release date: 2020-11-21 -- Tag: [v4.1.2](https://github.com/go-acme/lego/releases/tag/v4.1.2) +## [v4.1.2](https://github.com/go-acme/lego/releases/tag/v4.1.2) (2020-11-21) ### Fixed - **[lib]** fix: preferred chain support. -## v4.1.1 - -- Release date: 2020-11-19 -- Tag: [v4.1.1](https://github.com/go-acme/lego/releases/tag/v4.1.1) +## [v4.1.1](https://github.com/go-acme/lego/releases/tag/v4.1.1) (2020-11-19) ### Fixed - **[dnsprovider]** otc: select correct zone if multiple returned - **[dnsprovider]** azure: fix target must be a non-nil pointer -## v4.1.0 - -- Release date: 2020-11-06 -- Tag: [v4.1.0](https://github.com/go-acme/lego/releases/tag/v4.1.0) +## [v4.1.0](https://github.com/go-acme/lego/releases/tag/v4.1.0) (2020-11-06) ### Added @@ -1161,19 +667,13 @@ Cancelled due to a CI issue, replaced by v4.5.2. - **[lib]** acme/api: use postAsGet instead of post for AccountService.Get - **[lib]** fix: use http.Header.Set method instead of Add. -## v4.0.1 - -- Release date: 2020-09-03 -- Tag: [v4.0.1](https://github.com/go-acme/lego/releases/tag/v4.0.1) +## [v4.0.1](https://github.com/go-acme/lego/releases/tag/v4.0.1) (2020-09-03) ### Fixed - **[dnsprovider]** exoscale: change dependency version. -## v4.0.0 - -- Release date: 2020-09-02 -- Tag: [v4.0.0](https://github.com/go-acme/lego/releases/tag/v4.0.0) +## [v4.0.0](https://github.com/go-acme/lego/releases/tag/v4.0.0) (2020-09-02) ### Added @@ -1190,10 +690,7 @@ Cancelled due to a CI issue, replaced by v4.5.2. - **[dnsprovider]** Removes old Linode provider - **[lib]** Removes `AddPreCheck` function -## v3.9.0 - -- Release date: 2020-09-01 -- Tag: [v3.9.0](https://github.com/go-acme/lego/releases/tag/v3.9.0) +## [v3.9.0](https://github.com/go-acme/lego/releases/tag/v3.9.0) (2020-09-01) ### Added @@ -1210,10 +707,7 @@ Cancelled due to a CI issue, replaced by v4.5.2. - **[dnsprovider]** namesilo: fix cleanup. -## v3.8.0 - -- Release date: 2020-07-02 -- Tag: [v3.8.0](https://github.com/go-acme/lego/releases/tag/v3.8.0) +## [v3.8.0](https://github.com/go-acme/lego/releases/tag/v3.8.0) (2020-07-02) ### Added @@ -1237,10 +731,7 @@ 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 - -- Release date: 2020-05-11 -- Tag: [v3.7.0](https://github.com/go-acme/lego/releases/tag/v3.7.0) +## [v3.7.0](https://github.com/go-acme/lego/releases/tag/v3.7.0) (2020-05-11) ### Added @@ -1263,10 +754,7 @@ Cancelled due to a CI issue, replaced by v4.5.2. - **[cli]** fix: renew path information. - **[cli]** Fix account storage location warning message -## v3.6.0 - -- Release date: 2020-04-24 -- Tag: [v3.6.0](https://github.com/go-acme/lego/releases/tag/v3.6.0) +## [v3.6.0](https://github.com/go-acme/lego/releases/tag/v3.6.0) (2020-04-24) ### Added @@ -1290,10 +778,7 @@ Cancelled due to a CI issue, replaced by v4.5.2. - **[dnsprovider]** ns1: fix missing domain in log - **[dnsprovider]** rimuhosting: use HTTP client from config. -## v3.5.0 - -- Release date: 2020-03-15 -- Tag: [v3.5.0](https://github.com/go-acme/lego/releases/tag/v3.5.0) +## [v3.5.0](https://github.com/go-acme/lego/releases/tag/v3.5.0) (2020-03-15) ### Added @@ -1316,10 +801,7 @@ Cancelled due to a CI issue, replaced by v4.5.2. - **[dnsprovider]** gcloud: fixes issues when used with GKE Workload Identity - **[dnsprovider]** oraclecloud: fix subdomain support -## v3.4.0 - -- Release date: 2020-02-25 -- Tag: [v3.4.0](https://github.com/go-acme/lego/releases/tag/v3.4.0) +## [v3.4.0](https://github.com/go-acme/lego/releases/tag/v3.4.0) (2020-02-25) ### Added @@ -1344,10 +826,7 @@ 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) +## [v3.3.0](https://github.com/go-acme/lego/releases/tag/v3.3.0) (2020-01-08) ### Added @@ -1363,10 +842,7 @@ Cancelled due to a CI issue, replaced by v4.5.2. - **[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) +## [v3.2.0](https://github.com/go-acme/lego/releases/tag/v3.2.0) (2019-11-10) ### Added @@ -1382,10 +858,7 @@ Cancelled due to a CI issue, replaced by v4.5.2. - **[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) +## [v3.1.0](https://github.com/go-acme/lego/releases/tag/v3.1.0) (2019-10-07) ### Added @@ -1403,54 +876,36 @@ Cancelled due to a CI issue, replaced by v4.5.2. - **[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) +## [v3.0.2](https://github.com/go-acme/lego/releases/tag/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](https://github.com/go-acme/lego/releases/tag/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) +## [v3.0.0](https://github.com/go-acme/lego/releases/tag/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) +## [v2.7.2](https://github.com/go-acme/lego/releases/tag/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) +## [v2.7.1](https://github.com/go-acme/lego/releases/tag/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) +## [v2.7.0](https://github.com/go-acme/lego/releases/tag/v2.7.0) (2019-07-17) ### Added @@ -1467,10 +922,7 @@ There was a problem when creating the tag v3.0.1, this tag has been invalidated. - **[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) +## [v2.6.0](https://github.com/go-acme/lego/releases/tag/v2.6.0) (2019-05-27) ### Added @@ -1492,10 +944,7 @@ There was a problem when creating the tag v3.0.1, this tag has been invalidated. - **[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) +## [v2.5.0](https://github.com/go-acme/lego/releases/tag/v2.5.0) (2019-04-17) ### Added @@ -1514,12 +963,9 @@ There was a problem when creating the tag v3.0.1, this tag has been invalidated. - **[dnsprovider]** Disable authz when solve fail. - Add tzdata to the Docker image. -## v2.4.0 +## [v2.4.0](https://github.com/go-acme/lego/releases/tag/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. +- Migrate from xenolf/lego to go-acme/lego. ### Added @@ -1532,10 +978,7 @@ Migrate from xenolf/lego to go-acme/lego. - **[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) +## [v2.3.0](https://github.com/go-acme/lego/releases/tag/v2.3.0) (2019-03-11) ### Added @@ -1559,10 +1002,7 @@ 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) +## [v2.2.0](https://github.com/go-acme/lego/releases/tag/v2.2.0) (2019-02-08) ### Added @@ -1582,10 +1022,7 @@ Migrate from xenolf/lego to go-acme/lego. - **[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) +## [v2.1.0](https://github.com/go-acme/lego/releases/tag/v2.1.0) (2019-01-24) ### Added @@ -1602,10 +1039,7 @@ Migrate from xenolf/lego to go-acme/lego. - **[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) +## [v2.0.0](https://github.com/go-acme/lego/releases/tag/v2.0.0) (2019-01-09) ### Added @@ -1657,10 +1091,7 @@ 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) +## [v1.2.0](https://github.com/go-acme/lego/releases/tag/v1.2.0) (2018-11-04) ### Added @@ -1681,10 +1112,7 @@ 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) +## [v1.1.0](https://github.com/go-acme/lego/releases/tag/v1.1.0) (2018-10-16) ### Added @@ -1720,10 +1148,7 @@ Migrate from xenolf/lego to go-acme/lego. - **[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) +## [v1.0.0](https://github.com/go-acme/lego/releases/tag/v1.0.0) (2018-05-30) ### Changed @@ -1732,10 +1157,7 @@ Migrate from xenolf/lego to go-acme/lego. - **[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) +## [v0.5.0](https://github.com/go-acme/lego/releases/tag/v0.5.0) (2018-05-29) ### Added @@ -1769,10 +1191,7 @@ 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) +## [0.4.1](https://github.com/go-acme/lego/releases/tag/0.4.1) (2017-09-26) ### Added @@ -1785,10 +1204,7 @@ Migrate from xenolf/lego to go-acme/lego. - 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) +## [0.4.0](https://github.com/go-acme/lego/releases/tag/0.4.0) (2017-07-13) ### Added @@ -1841,10 +1257,7 @@ 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) +## [0.3.1](https://github.com/go-acme/lego/releases/tag/0.3.1) (2016-04-19) ### Added @@ -1856,10 +1269,7 @@ Migrate from xenolf/lego to go-acme/lego. - 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) +## [0.3.0](https://github.com/go-acme/lego/releases/tag/0.3.0) (2016-03-19) ### Added @@ -1894,10 +1304,7 @@ Migrate from xenolf/lego to go-acme/lego. - 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) +## [0.2.0](https://github.com/go-acme/lego/releases/tag/0.2.0) (2016-01-09) ### Added @@ -1927,10 +1334,7 @@ Migrate from xenolf/lego to go-acme/lego. - 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) +## [0.1.1](https://github.com/go-acme/lego/releases/tag/0.1.1) (2015-12-18) ### Added @@ -1950,9 +1354,6 @@ Migrate from xenolf/lego to go-acme/lego. - lib: Fix possible DOS on GetOCSPForCert -## 0.1.0 +## [0.1.0](https://github.com/go-acme/lego/releases/tag/0.1.0) (2015-12-03) -- Release date: 2015-12-03 -- Tag: [0.1.0](https://github.com/go-acme/lego/releases/tag/0.1.0) - -Initial release +- Initial release 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/Makefile b/Makefile index 8536dfc40..28cb33908 100644 --- a/Makefile +++ b/Makefile @@ -54,10 +54,10 @@ detach: .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..cebd033c2 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,114 +49,77 @@ 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). - - - - - - - - - - - - - - - - - - - + - - - - + + - - - + - + - - + - + - - - - + - - - - - + - - - - - - + + - - @@ -182,36 +138,26 @@ If your DNS provider is not supported, please open an [issue](https://github.com - - - - - - + - - - + - + - - - - + @@ -219,89 +165,79 @@ If your DNS provider is not supported, please open an [issue](https://github.com - - + - + - - + - + - + - - - + - + - - - - - + - + - - + - - - + - + - - + + +
35.com/三五互联Active24 Akamai EdgeDNS Alibaba Cloud DNS
AlibabaCloud ESA all-inklAlwaysdata Amazon Lightsail
Amazon Route 53Anexia CloudDNSANS SafeDNSArtFiles
ArvanCloud Aurora DNS AutodnsAxelname
Azion Azure (deprecated) Azure DNSBaidu Cloud
Beget.comBinary Lane Bindman Bluecat
Bluecat v2BookMyName Brandit (deprecated) Bunny
Checkdomain Civo
Cloud.ru CloudDNS
Cloudflare ClouDNSCloudXNS (Deprecated)ConoHa v2
ConoHa v3CloudXNS (Deprecated)ConoHa Constellix Core-NetworksCPanel/WHM
CzechiaDDnss (DynDNS Service)CPanel/WHM Derak Cloud deSEC.io
Designate DNSaaS for Openstack
Digital Ocean DirectAdmin DNS Made Easy
DNSExit dnsHome.de
DNSimple DNSPod (deprecated)
Domain Offensive (do.de) Domeneshop
DreamHost Duck DNS
DynDynDnsFree.de DynuEasyDNS
EdgeCenterEasyDNS Efficient IP EpikEuroDNS
Excedo ExoscaleExternal programF5 XC
External program freemyip.comFusionLayer NameSurfer G-Core Gandi
Gandi Live DNS (v5)Gigahost.no Glesys Go Daddy
Google CloudGoogle DomainsGravityHetzner
Google DomainsHetzner Hosting.deHosting.nlHostinger Hosttech
HTTP requestINWX
IonosIonos Cloud IPv64ISPConfig 3
ISPConfig 3 - Dynamic DNS (DDNS) Moduleiwantmyname (Deprecated)JD Cloudiwantmyname Joker
Joohoi's ACME-DNSKeyHelpLeaseweb Liara
Lima-City Linode (v4)
Liquid Web Loopia
LuaDNS Mail-in-a-Box
ManageEngine CloudDNS Manual
MetanameMetaregistrar mijn.hostMittwald
myaddr.{tools,dev,io}Mittwald MyDNS.jp MythicBeasts Name.comNamecheap Namesilo NearlyFreeSpeech.NETNeodigit
Netcup
Netlify Nicmanager NIFCloud
Njalla
Nodion NS1Octenium
Open Telekom Cloud Oracle Cloud
OVH plesk.com
Porkbun PowerDNS
Rackspace Rain Yun/雨云
RcodeZero reg.ru
Regfish RFC2136
RimuHostingRU CENTER Sakura CloudScaleway
Scaleway Selectel Selectel v2 SelfHost.(de|eu)Servercow
Servercow Shellrent Simply.com SonicSpaceship
StackpathSyse Technitium Tencent Cloud DNS
Tencent EdgeOne Timeweb CloudTodayNIC/时代互联
TransIP
UKFast SafeDNS UltradnsUnited-Domains VariomediaVegaDNS
VegaDNS Vercel Versio.[nl|eu|uk] VinylDNSVirtualname
VK Cloud Volcano Engine/火山引擎 Vscale Vultr
webnames.cawebnames.ruWebnames Websupport WEDOS
West.cn/西部数码
Yandex 360 Yandex Cloud Yandex PDD
Zone.eeZoneEdit
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..a1e8c68ab 100644 --- a/acme/api/internal/sender/useragent.go +++ b/acme/api/internal/sender/useragent.go @@ -4,10 +4,10 @@ package sender const ( // ourUserAgent is the User-Agent of this underlying library package. - ourUserAgent = "xenolf-acme/4.32.0" + ourUserAgent = "xenolf-acme/4.21.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..5118912f8 100644 --- a/certificate/authorization.go +++ b/certificate/authorization.go @@ -29,7 +29,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 +52,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 +62,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..fc139937b 100644 --- a/certificate/certificates.go +++ b/certificate/certificates.go @@ -65,26 +65,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 +89,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 } @@ -125,7 +108,6 @@ type CertifierOptions struct { KeyType certcrypto.KeyType Timeout time.Duration OverallRequestLimit int - DisableCommonName bool } // Certifier A service to obtain/renew/revoke certificates. @@ -172,7 +154,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 +179,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 +220,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 +245,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 +264,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 +274,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 +296,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 +435,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 +452,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 +505,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 +530,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 +668,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 +681,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 +689,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..ab215923d 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() @@ -72,7 +71,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 +85,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..a8d678af2 100644 --- a/challenge/dns01/nameserver.go +++ b/challenge/dns01/nameserver.go @@ -81,7 +81,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 +89,6 @@ func ParseNameservers(servers []string) []string { resolvers = append(resolvers, resolver) } } - return resolvers } @@ -134,7 +132,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,7 +148,6 @@ func FindZoneByFqdnCustom(fqdn string, nameservers []string) (string, error) { if err != nil { return "", fmt.Errorf("[fqdn=%s] %w", fqdn, err) } - return soa.zone, nil } @@ -176,12 +172,13 @@ func lookupSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) } 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 +232,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 +267,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..706e8dbec 100644 --- a/challenge/dns01/precheck.go +++ b/challenge/dns01/precheck.go @@ -9,10 +9,6 @@ import ( "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) @@ -29,7 +25,6 @@ func WrapPreCheck(wrap WrapPreCheckFunc) ChallengeOption { } // DisableCompletePropagationRequirement obsolete. -// // Deprecated: use DisableAuthoritativeNssPropagationRequirement instead. func DisableCompletePropagationRequirement() ChallengeOption { return DisableAuthoritativeNssPropagationRequirement() @@ -126,7 +121,7 @@ func (p preCheck) checkDNSPropagation(fqdn, value string) (bool, error) { func checkNameserversPropagation(fqdn, value string, nameservers []string, addPort bool) (bool, error) { for _, ns := range nameservers { if addPort { - ns = net.JoinHostPort(ns, defaultNameserverPort) + ns = net.JoinHostPort(ns, "53") } r, err := dnsQuery(fqdn, dns.TypeTXT, []string{ns}, false) @@ -141,11 +136,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..1f3ecbf7e 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, _ := checkNameserversPropagation(test.fqdn, test.value, test.ns, true) + 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 := checkNameserversPropagation(test.fqdn, test.value, test.ns, true) + 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..c31aeed6a 100644 --- a/challenge/http01/domain_matcher.go +++ b/challenge/http01/domain_matcher.go @@ -88,7 +88,6 @@ func (m *forwardedMatcher) matches(r *http.Request, domain string) bool { } host := fwds[0]["host"] - return matchDomain(host, domain) } @@ -100,7 +99,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 +110,6 @@ func parseForwardedHeader(s string) (elements []map[string]string, err error) { pos = i inquote = false } - continue } @@ -121,7 +118,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 @@ -141,7 +137,6 @@ func parseForwardedHeader(s string) (elements []map[string]string, err error) { val = s[pos:i] cur[key] = val } - elements = append(elements, cur) cur = make(map[string]string) key = "" @@ -164,14 +159,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,7 +178,6 @@ func skipWS(s string, i int) int { for isWS(rune(s[i+1])) { i++ } - return i } diff --git a/challenge/http01/domain_matcher_test.go b/challenge/http01/domain_matcher_test.go index 7bedf9f63..efdc4641d 100644 --- a/challenge/http01/domain_matcher_test.go +++ b/challenge/http01/domain_matcher_test.go @@ -77,7 +77,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) 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..009271cec 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) @@ -121,7 +120,6 @@ func (s *ProviderServer) serve(domain, token, keyAuth string) { } log.Infof("[%s] Served key authentication", domain) - return } 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..b3e4986dd 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,13 +68,8 @@ type AccountsStorage struct { // NewAccountsStorage Creates a new AccountsStorage. func NewAccountsStorage(ctx *cli.Context) *AccountsStorage { - // TODO: move to account struct? - email := ctx.String(flgEmail) - - userID := email - if userID == "" { - userID = userIDPlaceholder - } + // TODO: move to account struct? Currently MUST pass email. + email := getEmail(ctx) serverURL, err := url.Parse(ctx.String(flgServer)) if err != nil { @@ -85,11 +79,10 @@ func NewAccountsStorage(ctx *cli.Context) *AccountsStorage { rootPath := filepath.Join(ctx.String(flgPath), 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,12 +209,16 @@ 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) { @@ -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..f9bcdade8 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" @@ -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_dnshelp.go b/cmd/cmd_dnshelp.go index 41adf4c8d..1a61cac80 100644 --- a/cmd/cmd_dnshelp.go +++ b/cmd/cmd_dnshelp.go @@ -58,7 +58,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 +66,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..bf7b232da 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" @@ -37,7 +36,7 @@ func createList() *cli.Command { // fake email, needed by NewAccountsStorage &cli.StringFlag{ Name: flgEmail, - Value: "", + Value: "unknown", Hidden: true, }, }, @@ -68,7 +67,6 @@ func listCertificates(ctx *cli.Context) error { if !names { fmt.Println("No certificates found.") } - return nil } @@ -101,11 +99,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 +122,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 +129,6 @@ func listAccount(ctx *cli.Context) error { } var account Account - err = json.Unmarshal(data, &account) if err != nil { return err @@ -156,12 +147,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..c4c680234 100644 --- a/cmd/cmd_renew.go +++ b/cmd/cmd_renew.go @@ -20,17 +20,25 @@ import ( // Flag names. const ( - flgRenewDays = "days" - flgRenewDynamic = "dynamic" + flgDays = "days" 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" ) +const ( + 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 { return &cli.Command{ Name: "renew", @@ -39,37 +47,27 @@ func createRenew() *cli.Command { 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) != "" if hasDomains && hasCsr { - log.Fatalf("Please specify either --%s/-d or --%s/-c, but not both", flgDomains, flgCSR) + log.Fatal("Please specify either --%s/-d or --%s/-c, but not both", flgDomains, flgCSR) } - if !hasDomains && !hasCsr { - log.Fatalf("Please specify --%s/-d (or --%s/-c if you already have a CSR)", flgDomains, flgCSR) + log.Fatal("Please specify --%s/-d (or --%s/-c if you already have a CSR)", flgDomains, flgCSR) } - if ctx.Bool(flgForceCertDomains) && hasCsr { - log.Fatalf("--%s only works with --%s/-d, --%s/-c doesn't support this option.", flgForceCertDomains, flgDomains, flgCSR) + log.Fatal("--%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: flgDays, 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.", + Usage: "Do not use the renewalInfo endpoint (draft-ietf-acme-ari) to check if a certificate should be renewed.", }, &cli.DurationFlag{ Name: flgARIWaitToRenewDuration, @@ -103,10 +101,6 @@ func createRenew() *cli.Command { 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, Usage: "Force the authorizations to be relinquished even if the certificate request was successful.", @@ -115,11 +109,6 @@ func createRenew() *cli.Command { Name: flgRenewHook, 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, Usage: "Do not add a random sleep before the renewal." + @@ -144,9 +133,7 @@ func renew(ctx *cli.Context) error { bundle := !ctx.Bool(flgNoBundle) - meta := map[string]string{ - hookEnvAccountEmail: account.Email, - } + meta := map[string]string{renewEnvAccountEmail: account.Email} // CSR if ctx.IsSet(flgCSR) { @@ -171,10 +158,8 @@ func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyT cert := certificates[0] - var ( - ariRenewalTime *time.Time - replacesCertID string - ) + var ariRenewalTime *time.Time + var replacesCertID string var client *lego.Client @@ -202,7 +187,7 @@ func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyT certDomains := certcrypto.ExtractDomains(cert) - if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgRenewDays), ctx.Bool(flgRenewDynamic)) && + if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgDays)) && (!forceDomains || slices.Equal(certDomains, domains)) { return nil } @@ -216,7 +201,6 @@ func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyT log.Infof("[%s] acme: Trying renewal with %d hours remaining", domain, int(timeLeft.Hours())) var privateKey crypto.PrivateKey - if ctx.Bool(flgReuseKey) { keyBytes, errR := certsStorage.ReadFile(domain, keyExt) if errR != nil { @@ -234,7 +218,6 @@ func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyT if !isatty.IsTerminal(os.Stdout.Fd()) && !ctx.Bool(flgNoRandomSleep) { // 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,7 +225,7 @@ func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyT time.Sleep(sleepTime) } - renewalDomains := slices.Clone(domains) + renewalDomains := domains if !forceDomains { renewalDomains = merge(certDomains, domains) } @@ -255,7 +238,6 @@ func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyT NotAfter: getTime(ctx, flgNotAfter), Bundle: bundle, PreferredChain: ctx.String(flgPreferredChain), - Profile: ctx.String(flgProfile), AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations), } @@ -268,13 +250,11 @@ 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(flgRenewHook), meta) } func renewForCSR(ctx *cli.Context, account *Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error { @@ -298,10 +278,8 @@ func renewForCSR(ctx *cli.Context, account *Account, keyType certcrypto.KeyType, cert := certificates[0] - var ( - ariRenewalTime *time.Time - replacesCertID string - ) + var ariRenewalTime *time.Time + var replacesCertID string var client *lego.Client @@ -325,7 +303,7 @@ func renewForCSR(ctx *cli.Context, account *Account, keyType certcrypto.KeyType, } } - if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgRenewDays), ctx.Bool(flgRenewDynamic)) { + if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgDays)) { return nil } @@ -343,7 +321,6 @@ func renewForCSR(ctx *cli.Context, account *Account, keyType certcrypto.KeyType, NotAfter: getTime(ctx, flgNotAfter), Bundle: bundle, PreferredChain: ctx.String(flgPreferredChain), - Profile: ctx.String(flgProfile), AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations), } @@ -360,51 +337,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(flgRenewHook), 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 +370,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)) 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,6 +389,24 @@ func getARIRenewalTime(ctx *cli.Context, cert *x509.Certificate, domain string, return renewalTime } +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) + + 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 { if slices.Contains(prevDomains, next) { 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_run.go b/cmd/cmd_run.go index 5924c4b66..f2cec5655 100644 --- a/cmd/cmd_run.go +++ b/cmd/cmd_run.go @@ -20,12 +20,9 @@ const ( 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 { @@ -35,16 +32,13 @@ func createRun() *cli.Command { 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) != "" 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, @@ -68,19 +62,11 @@ func createRun() *cli.Command { 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, 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, Usage: "Force the authorizations to be relinquished even if the certificate request was successful.", @@ -89,24 +75,19 @@ func createRun() *cli.Command { Name: flgRunHook, 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 { @@ -143,12 +124,12 @@ func run(ctx *cli.Context) error { certsStorage.SaveResource(cert) meta := map[string]string{ - hookEnvAccountEmail: account.Email, + renewEnvAccountEmail: account.Email, } addPathToMetadata(meta, cert.Domain, cert, certsStorage) - return launchHook(ctx.String(flgRunHook), ctx.Duration(flgRunHookTimeout), meta) + return launchHook(ctx.String(flgRunHook), meta) } func handleTOS(ctx *cli.Context, client *lego.Client) bool { @@ -158,12 +139,10 @@ func handleTOS(ctx *cli.Context, client *lego.Client) bool { } 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) @@ -213,22 +192,20 @@ func obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Reso // 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, + MustStaple: ctx.Bool(flgMustStaple), PreferredChain: ctx.String(flgPreferredChain), - Profile: ctx.String(flgProfile), AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations), } - if ctx.IsSet(flgPrivateKey) { - var err error + notBefore := ctx.Timestamp(flgNotBefore) + 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(flgNotAfter) + if notAfter != nil { + request.NotAfter = *notAfter } return client.Certificate.Obtain(request) @@ -247,18 +224,8 @@ func obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Reso NotAfter: getTime(ctx, flgNotAfter), 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) - } - } - return client.Certificate.ObtainForCSR(request) } diff --git a/cmd/flags.go b/cmd/flags.go index c7e8371b6..0a8024dff 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -16,7 +16,6 @@ const ( flgServer = "server" flgAcceptTOS = "accept-tos" flgEmail = "email" - flgDisableCommonName = "disable-cn" flgCSR = "csr" flgEAB = "eab" flgKID = "kid" @@ -26,14 +25,12 @@ const ( 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" @@ -52,18 +49,6 @@ const ( 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{ @@ -74,7 +59,7 @@ func CreateFlags(defaultPath string) []cli.Flag { &cli.StringFlag{ Name: flgServer, 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, }, @@ -86,13 +71,8 @@ func CreateFlags(defaultPath string) []cli.Flag { &cli.StringFlag{ Name: flgEmail, 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, Aliases: []string{"c"}, @@ -100,17 +80,17 @@ func CreateFlags(defaultPath string) []cli.Flag { }, &cli.BoolFlag{ Name: flgEAB, - EnvVars: []string{envEAB}, + EnvVars: []string{"LEGO_EAB"}, Usage: "Use External Account Binding for account registration. Requires --kid and --hmac.", }, &cli.StringFlag{ Name: flgKID, - EnvVars: []string{envEABKID}, + EnvVars: []string{"LEGO_EAB_KID"}, Usage: "Key identifier from External CA. Used for External Account Binding.", }, &cli.StringFlag{ Name: flgHMAC, - EnvVars: []string{envEABHMAC}, + 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{ @@ -125,7 +105,7 @@ func CreateFlags(defaultPath string) []cli.Flag { }, &cli.StringFlag{ Name: flgPath, - EnvVars: []string{envPath}, + EnvVars: []string{"LEGO_PATH"}, Usage: "Directory to use for storing the data.", Value: defaultPath, }, @@ -138,11 +118,6 @@ func CreateFlags(defaultPath string) []cli.Flag { 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, Usage: "Validate against this HTTP header when solving HTTP-01 based challenges behind a reverse proxy.", @@ -170,11 +145,6 @@ func CreateFlags(defaultPath string) []cli.Flag { 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, 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.", @@ -222,19 +192,19 @@ func CreateFlags(defaultPath string) []cli.Flag { &cli.BoolFlag{ Name: flgPFX, 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, 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, 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, @@ -258,6 +228,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..61a3d532a 100644 --- a/cmd/lego/main.go +++ b/cmd/lego/main.go @@ -26,7 +26,6 @@ func main() { } 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 index cf9ad00ef..c23e0d038 100644 --- a/cmd/lego/zz_gen_version.go +++ b/cmd/lego/zz_gen_version.go @@ -2,7 +2,7 @@ package main -const defaultVersion = "v4.32.0+dev-detach" +const defaultVersion = "v4.21.0+dev-release" var version = "" diff --git a/cmd/setup.go b/cmd/setup.go index 6d15adad3..3fc05038e 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -1,18 +1,14 @@ 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" @@ -40,7 +36,7 @@ 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 @@ -54,7 +50,6 @@ func newClient(ctx *cli.Context, acc registration.User, keyType certcrypto.KeyTy KeyType: keyType, Timeout: time.Duration(ctx.Int(flgCertTimeout)) * time.Second, OverallRequestLimit: ctx.Int(flgOverallRequestLimit), - DisableCommonName: ctx.Bool(flgDisableCommonName), } config.UserAgent = getUserAgent(ctx) @@ -74,12 +69,6 @@ func newClient(ctx *cli.Context, acc registration.User, keyType certcrypto.KeyTy 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() @@ -114,10 +103,17 @@ func getKeyType(ctx *cli.Context) certcrypto.KeyType { } log.Fatalf("Unsupported KeyType: %s", keyType) - return "" } +func getEmail(ctx *cli.Context) string { + email := ctx.String(flgEmail) + if email == "" { + log.Fatalf("You have to pass an account (email address) to the program using --%s or -m", flgEmail) + } + return email +} + func getUserAgent(ctx *cli.Context) string { return strings.TrimSpace(fmt.Sprintf("%s lego-cli/%s", ctx.String(flgUserAgent), ctx.App.Version)) } @@ -128,7 +124,6 @@ func createNonExistingFolder(path string) error { } else if err != nil { return err } - return nil } @@ -137,12 +132,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 +157,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..0a59099a8 100644 --- a/cmd/setup_challenges.go +++ b/cmd/setup_challenges.go @@ -25,14 +25,14 @@ func setupChallenges(ctx *cli.Context, client *lego.Client) { } if ctx.Bool(flgHTTP) { - err := client.Challenge.SetHTTP01Provider(setupHTTPProvider(ctx), http01.SetDelay(ctx.Duration(flgHTTPDelay))) + 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))) + err := client.Challenge.SetTLSALPN01Provider(setupTLSProvider(ctx)) if err != nil { log.Fatal(err) } @@ -54,21 +54,18 @@ func setupHTTPProvider(ctx *cli.Context) challenge.Provider { if err != nil { log.Fatal(err) } - return ps case ctx.IsSet(flgHTTPMemcachedHost): ps, err := memcached.NewMemcachedProvider(ctx.StringSlice(flgHTTPMemcachedHost)) if err != nil { log.Fatal(err) } - return ps case ctx.IsSet(flgHTTPS3Bucket): ps, err := s3.NewHTTPProvider(ctx.String(flgHTTPS3Bucket)) if err != nil { log.Fatal(err) } - return ps case ctx.IsSet(flgHTTPPort): iface := ctx.String(flgHTTPPort) @@ -85,14 +82,12 @@ func setupHTTPProvider(ctx *cli.Context) challenge.Provider { if header := ctx.String(flgHTTPProxyHeader); header != "" { srv.SetProxyHeader(header) } - return srv case ctx.Bool(flgHTTP): srv := http01.NewProviderServer("", "") if header := ctx.String(flgHTTPProxyHeader); header != "" { srv.SetProxyHeader(header) } - return srv default: log.Fatal("Invalid HTTP challenge options.") 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..e5ae3b46d 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -12,28 +12,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 +32,15 @@ func allDNSCodes() string { "cloudns", "cloudru", "cloudxns", - "com35", "conoha", - "conohav3", "constellix", "corenetworks", "cpanel", - "czechia", - "ddnss", "derak", "desec", "designate", "digitalocean", "directadmin", - "dnsexit", "dnshomede", "dnsimple", "dnsmadeeasy", @@ -66,33 +50,23 @@ 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", @@ -107,15 +81,9 @@ func allDNSCodes() string { "internetbs", "inwx", "ionos", - "ionoscloud", "ipv64", - "ispconfig", - "ispconfigddns", "iwantmyname", - "jdcloud", "joker", - "keyhelp", - "leaseweb", "liara", "lightsail", "limacity", @@ -125,29 +93,22 @@ func allDNSCodes() string { "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", @@ -172,35 +133,28 @@ func allDNSCodes() string { "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 +176,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 +191,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 +218,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 +237,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 +259,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 +281,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 +308,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 +335,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 +359,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,61 +382,15 @@ 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_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_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_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).`) @@ -712,10 +404,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 +424,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 +444,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 +464,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 +485,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 +512,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 (in seconds)`) + ew.writeln(` - "CLOUDFLARE_POLLING_INTERVAL": Time between DNS propagation check (in seconds)`) + ew.writeln(` - "CLOUDFLARE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation (in seconds)`) + ew.writeln(` - "CLOUDFLARE_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/cloudflare`) @@ -843,11 +533,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,11 +556,11 @@ 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`) @@ -888,38 +578,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 +600,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,10 +622,10 @@ 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`) @@ -997,11 +643,11 @@ func displayDNSHelp(w io.Writer, name string) error { 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(` - "CORENETWORKS_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "CORENETWORKS_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "CORENETWORKS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "CORENETWORKS_SEQUENCE_INTERVAL": Time between sequential requests`) + ew.writeln(` - "CORENETWORKS_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/corenetworks`) @@ -1020,56 +666,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 +688,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 +709,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,9 +737,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_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(` - "DESIGNATE_ZONE_NAME": The zone name to use in the OpenStack Project to manage TXT records.`) ew.writeln(` - "OS_PROJECT_ID": Project ID`) ew.writeln(` - "OS_TENANT_NAME": Tenant name (deprecated see OS_PROJECT_NAME and OS_PROJECT_ID)`) @@ -1154,10 +760,10 @@ 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`) @@ -1176,35 +782,15 @@ func displayDNSHelp(w io.Writer, name string) error { 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_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "DIRECTADMIN_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "DIRECTADMIN_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "DIRECTADMIN_TTL": The TTL of the TXT record used for the DNS challenge`) 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.`) @@ -1217,10 +803,10 @@ func displayDNSHelp(w io.Writer, name string) error { 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(` - "DNSHOMEDE_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "DNSHOMEDE_POLLING_INTERVAL": Time between DNS propagation checks`) + ew.writeln(` - "DNSHOMEDE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation; defaults to 300s (5 minutes)`) + ew.writeln(` - "DNSHOMEDE_SEQUENCE_INTERVAL": Time between sequential requests`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/dnshomede`) @@ -1238,9 +824,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 +844,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 +865,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 +885,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 +907,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 +926,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 +946,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 +969,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 +989,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 +1011,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 +1037,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 +1059,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 +1081,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.`) @@ -1630,37 +1113,14 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) 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 +1133,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 +1154,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 +1175,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 +1199,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 +1219,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 +1240,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 +1261,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 +1281,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 +1296,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 +1320,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 +1342,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 +1362,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,10 +1384,10 @@ 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() @@ -2032,10 +1407,10 @@ func displayDNSHelp(w io.Writer, name string) error { 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(` - "HUAWEICLOUD_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "HUAWEICLOUD_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "HUAWEICLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "HUAWEICLOUD_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/huaweicloud`) @@ -2052,10 +1427,10 @@ func displayDNSHelp(w io.Writer, name string) error { 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(` - "HURRICANE_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "HURRICANE_POLLING_INTERVAL": Time between DNS propagation checks`) + ew.writeln(` - "HURRICANE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation; defaults to 300s (5 minutes)`) + ew.writeln(` - "HURRICANE_SEQUENCE_INTERVAL": Time between sequential requests`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/hurricane`) @@ -2069,12 +1444,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 +1462,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 +1488,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 +1509,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 +1530,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 +1555,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 +1576,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 +1597,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 +1618,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 +1638,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 +1659,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 +1682,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 +1703,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,8 +1726,8 @@ 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`) @@ -2498,11 +1744,11 @@ func displayDNSHelp(w io.Writer, name string) error { 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(` - "LIMACITY_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "LIMACITY_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "LIMACITY_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "LIMACITY_SEQUENCE_INTERVAL": Time between sequential requests`) + ew.writeln(` - "LIMACITY_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/limacity`) @@ -2519,10 +1765,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 +1786,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 +1810,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 +1831,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,9 +1853,8 @@ 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`) @@ -2627,23 +1872,14 @@ func displayDNSHelp(w io.Writer, name string) error { 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(` - "MANAGEENGINE_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "MANAGEENGINE_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "MANAGEENGINE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "MANAGEENGINE_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/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,33 +1893,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.`) @@ -2696,11 +1912,11 @@ func displayDNSHelp(w io.Writer, name string) error { 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(` - "MIJNHOST_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "MIJNHOST_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "MIJNHOST_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "MIJNHOST_SEQUENCE_INTERVAL": Time between sequential requests`) + ew.writeln(` - "MIJNHOST_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/mijnhost`) @@ -2717,36 +1933,15 @@ func displayDNSHelp(w io.Writer, name string) error { 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(` - "MITTWALD_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "MITTWALD_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "MITTWALD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "MITTWALD_SEQUENCE_INTERVAL": Time between sequential requests`) + ew.writeln(` - "MITTWALD_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/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 +1955,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 +1978,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 +1999,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 +2021,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 +2041,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 +2061,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 +2084,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 +2104,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 +2127,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 +2150,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 +2170,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 +2190,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 +2210,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 +2227,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 +2252,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`) @@ -3184,10 +2286,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 +2308,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 +2331,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 +2352,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,10 +2373,10 @@ 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`) @@ -3291,10 +2393,10 @@ func displayDNSHelp(w io.Writer, name string) error { 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(` - "RAINYUN_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "RAINYUN_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "RAINYUN_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "RAINYUN_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/rainyun`) @@ -3311,10 +2413,10 @@ 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`) @@ -3331,10 +2433,10 @@ func displayDNSHelp(w io.Writer, name string) error { 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(` - "REGFISH_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "REGFISH_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "REGFISH_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "REGFISH_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/regfish`) @@ -3352,12 +2454,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`) @@ -3377,12 +2479,12 @@ func displayDNSHelp(w io.Writer, name string) error { 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_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_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_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 +2501,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 +2530,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 +2550,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 +2571,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 +2593,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 +2613,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,14 +2636,11 @@ 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`) @@ -3562,10 +2659,10 @@ func displayDNSHelp(w io.Writer, name string) error { 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(` - "SELFHOSTDE_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "SELFHOSTDE_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "SELFHOSTDE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "SELFHOSTDE_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/selfhostde`) @@ -3583,10 +2680,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 +2701,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 +2722,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 +2743,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,33 +2766,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.`) @@ -3730,10 +2786,10 @@ func displayDNSHelp(w io.Writer, name string) error { 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(` - "TECHNITIUM_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "TECHNITIUM_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "TECHNITIUM_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "TECHNITIUM_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/technitium`) @@ -3751,12 +2807,12 @@ 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`) @@ -3773,34 +2829,13 @@ func displayDNSHelp(w io.Writer, name string) error { 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(` - "TIMEWEBCLOUD_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "TIMEWEBCLOUD_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "TIMEWEBCLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) 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 +2849,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 +2870,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 +2889,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(` - "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_SEQUENCE_INTERVAL": Time between sequential requests`) + 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 +2912,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 +2931,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 +2954,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 +2977,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,9 +3001,9 @@ 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`) @@ -4030,12 +3022,12 @@ func displayDNSHelp(w io.Writer, name string) error { 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_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "VOLC_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "VOLC_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) 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(` - "VOLC_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/volcengine`) @@ -4053,10 +3045,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 +3065,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 +3106,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,10 +3128,10 @@ 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`) @@ -4177,10 +3149,10 @@ func displayDNSHelp(w io.Writer, name string) error { 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(` - "WESTCN_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "WESTCN_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "WESTCN_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "WESTCN_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/westcn`) @@ -4197,10 +3169,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 +3190,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 +3211,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 +3232,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 +3252,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..6d9fc3f1a 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -7,34 +7,23 @@ chapter: false 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 +- 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..7ccfeb53d 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. 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..0d57146ff 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" -d '*.example.com' -d example.com 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" %}}). -## 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..d822ecea6 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 -d '*.example.com' -d example.com 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 - -d '*.example.com' -d example.com run ``` @@ -45,7 +45,7 @@ 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) | @@ -57,12 +57,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). @@ -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..08e354f87 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 -d '*.example.com' -d example.com run ``` @@ -49,9 +49,9 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). 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..ff03f22e1 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_auroradns.md b/docs/content/dns/zz_gen_auroradns.md index d608c85bb..d3fa5a1df 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 -d '*.example.com' -d example.com run ``` @@ -50,9 +50,9 @@ 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" %}}). diff --git a/docs/content/dns/zz_gen_autodns.md b/docs/content/dns/zz_gen_autodns.md index f1f25e916..584f21770 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 -d '*.example.com' -d example.com run ``` @@ -51,10 +51,10 @@ 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" %}}). 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..e1ecd9506 100644 --- a/docs/content/dns/zz_gen_azure.md +++ b/docs/content/dns/zz_gen_azure.md @@ -51,10 +51,10 @@ 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. diff --git a/docs/content/dns/zz_gen_azuredns.md b/docs/content/dns/zz_gen_azuredns.md index 3b2586711..4b762e675 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 --email you@example.com --dns azuredns -d '*.example.com' -d example.com 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 --email you@example.com --dns azuredns -d '*.example.com' -d example.com run ### Using Azure CLI az login \ -lego --dns azuredns -d '*.example.com' -d example.com run +lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com run ### Using Managed Identity (Azure VM) AZURE_TENANT_ID= \ AZURE_RESOURCE_GROUP= \ -lego --dns azuredns -d '*.example.com' -d example.com run +lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com 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 --email you@example.com --dns azuredns -d '*.example.com' -d example.com run ``` @@ -83,13 +83,13 @@ 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. @@ -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..c74273a7f 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 -d '*.example.com' -d example.com run ``` @@ -47,9 +47,9 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_bluecat.md b/docs/content/dns/zz_gen_bluecat.md index 2d9eb5b48..3b0ebf898 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 -d '*.example.com' -d example.com run ``` @@ -56,11 +56,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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_HTTP_TIMEOUT` | API request timeout | +| `BLUECAT_POLLING_INTERVAL` | Time between DNS propagation check | +| `BLUECAT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `BLUECAT_SKIP_DEPLOY` | Skip deployements | -| `BLUECAT_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | +| `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" %}}). 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..c2264f71c 100644 --- a/docs/content/dns/zz_gen_brandit.md +++ b/docs/content/dns/zz_gen_brandit.md @@ -31,7 +31,7 @@ Here is an example bash command using the Brandit (deprecated) provider: ```bash BRANDIT_API_KEY=xxxxxxxxxxxxxxxxxxxxx \ BRANDIT_API_USERNAME=yyyyyyyyyyyyyyyyyyyy \ -lego --dns brandit -d '*.example.com' -d example.com run +lego --email you@example.com --dns brandit -d '*.example.com' -d example.com run ``` @@ -52,10 +52,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_bunny.md b/docs/content/dns/zz_gen_bunny.md index 63c30782a..f945b9153 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,9 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_checkdomain.md b/docs/content/dns/zz_gen_checkdomain.md index e0275f6c9..694b8cc67 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 -d '*.example.com' -d example.com run ``` @@ -48,10 +48,10 @@ 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" %}}). diff --git a/docs/content/dns/zz_gen_civo.md b/docs/content/dns/zz_gen_civo.md index 61303b539..73f04140d 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 -d '*.example.com' -d example.com run ``` @@ -47,9 +47,9 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_clouddns.md b/docs/content/dns/zz_gen_clouddns.md index d10d1d6a1..4754cebca 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 -d '*.example.com' -d example.com run ``` @@ -51,10 +51,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_cloudflare.md b/docs/content/dns/zz_gen_cloudflare.md index f3390a5fd..55fbaeae3 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 -d '*.example.com' -d example.com run # or CLOUDFLARE_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \ -lego --dns cloudflare -d '*.example.com' -d example.com run +lego --email you@example.com --dns cloudflare -d '*.example.com' -d example.com run ``` @@ -60,11 +60,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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 (in seconds) | +| `CLOUDFLARE_POLLING_INTERVAL` | Time between DNS propagation check (in seconds) | +| `CLOUDFLARE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation (in seconds) | +| `CLOUDFLARE_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" %}}). diff --git a/docs/content/dns/zz_gen_cloudns.md b/docs/content/dns/zz_gen_cloudns.md index 26bd838f2..f063d835f 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 -d '*.example.com' -d example.com run ``` @@ -49,11 +49,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_cloudru.md b/docs/content/dns/zz_gen_cloudru.md index 6dc3b0030..b4cb9dcac 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 -d '*.example.com' -d example.com run ``` @@ -51,11 +51,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_cloudxns.md b/docs/content/dns/zz_gen_cloudxns.md index b26e5ddb5..c63a773e1 100644 --- a/docs/content/dns/zz_gen_cloudxns.md +++ b/docs/content/dns/zz_gen_cloudxns.md @@ -28,7 +28,7 @@ Here is an example bash command using the CloudXNS (Deprecated) 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 -d '*.example.com' -d example.com run ``` @@ -49,10 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). 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..c5de0d20e 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 -d '*.example.com' -d example.com run ``` @@ -51,11 +51,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). @@ -65,7 +65,7 @@ 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..69040353d 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 -d '*.example.com' -d example.com run ``` @@ -49,10 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_corenetworks.md b/docs/content/dns/zz_gen_corenetworks.md index 05468b1a3..0b61bbc77 100644 --- a/docs/content/dns/zz_gen_corenetworks.md +++ b/docs/content/dns/zz_gen_corenetworks.md @@ -28,7 +28,7 @@ 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 +lego --email you@example.com --dns corenetworks -d '*.example.com' -d example.com run ``` @@ -49,11 +49,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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) | +| `CORENETWORKS_HTTP_TIMEOUT` | API request timeout | +| `CORENETWORKS_POLLING_INTERVAL` | Time between DNS propagation check | +| `CORENETWORKS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `CORENETWORKS_SEQUENCE_INTERVAL` | Time between sequential requests | +| `CORENETWORKS_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" %}}). diff --git a/docs/content/dns/zz_gen_cpanel.md b/docs/content/dns/zz_gen_cpanel.md index e5c0cc047..9e939ca59 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 -d '*.example.com' -d example.com 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 -d '*.example.com' -d example.com run ``` @@ -61,11 +61,12 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). 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..a5daf76db 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 you@example.com --dns derak -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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. diff --git a/docs/content/dns/zz_gen_desec.md b/docs/content/dns/zz_gen_desec.md index 4dbc713d6..45e5fabc6 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_designate.md b/docs/content/dns/zz_gen_designate.md index 9703f094d..cbbdfa557 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 -d '*.example.com' -d example.com 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 -d '*.example.com' -d example.com 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 -d '*.example.com' -d example.com run ``` @@ -74,9 +74,9 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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_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 | | `DESIGNATE_ZONE_NAME` | The zone name to use in the OpenStack Project to manage TXT records. | | `OS_PROJECT_ID` | Project ID | | `OS_TENANT_NAME` | Tenant name (deprecated see OS_PROJECT_NAME and OS_PROJECT_ID) | diff --git a/docs/content/dns/zz_gen_digitalocean.md b/docs/content/dns/zz_gen_digitalocean.md index 4dc43886d..3bf57f59d 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 -d '*.example.com' -d example.com run ``` @@ -48,10 +48,10 @@ 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" %}}). diff --git a/docs/content/dns/zz_gen_directadmin.md b/docs/content/dns/zz_gen_directadmin.md index 1d03dcc4e..252c69ccf 100644 --- a/docs/content/dns/zz_gen_directadmin.md +++ b/docs/content/dns/zz_gen_directadmin.md @@ -29,7 +29,7 @@ Here is an example bash command using the DirectAdmin provider: DIRECTADMIN_API_URL="http://example.com:2222" \ DIRECTADMIN_USERNAME=xxxx \ DIRECTADMIN_PASSWORD=yyy \ -lego --dns directadmin -d '*.example.com' -d example.com run +lego --email you@example.com --dns directadmin -d '*.example.com' -d example.com run ``` @@ -51,10 +51,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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_HTTP_TIMEOUT` | API request timeout | +| `DIRECTADMIN_POLLING_INTERVAL` | Time between DNS propagation check | +| `DIRECTADMIN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `DIRECTADMIN_TTL` | The TTL of the TXT record used for the DNS challenge | | `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. 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..56825f38d 100644 --- a/docs/content/dns/zz_gen_dnshomede.md +++ b/docs/content/dns/zz_gen_dnshomede.md @@ -27,10 +27,10 @@ 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 +lego --email you@example.com --dns dnshomede -d '*.example.com' -d example.com 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 -d my.example.org -d demo.example.org ``` @@ -50,10 +50,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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) | +| `DNSHOMEDE_HTTP_TIMEOUT` | API request timeout | +| `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 | The 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..188d7c895 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 -d '*.example.com' -d example.com run ``` @@ -48,9 +48,9 @@ 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" %}}). diff --git a/docs/content/dns/zz_gen_dnsmadeeasy.md b/docs/content/dns/zz_gen_dnsmadeeasy.md index e7f260889..d6f1cb56b 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 -d '*.example.com' -d example.com run ``` @@ -49,11 +49,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_dnspod.md b/docs/content/dns/zz_gen_dnspod.md index 86112a5ce..2a654d640 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_dode.md b/docs/content/dns/zz_gen_dode.md index 28eebe5fa..b73fa70df 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_domeneshop.md b/docs/content/dns/zz_gen_domeneshop.md index 0530ab365..24a19a056 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 -d '*.example.com' -d example.com run ``` @@ -49,9 +49,9 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_dreamhost.md b/docs/content/dns/zz_gen_dreamhost.md index b9d273099..9d9663971 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 -d '*.example.com' -d example.com run ``` @@ -47,9 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_duckdns.md b/docs/content/dns/zz_gen_duckdns.md index 8b60780d2..515097c77 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_dyn.md b/docs/content/dns/zz_gen_dyn.md index e31a90e45..32f902394 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 -d '*.example.com' -d example.com run ``` @@ -51,10 +51,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). 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..d59fa23f5 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_easydns.md b/docs/content/dns/zz_gen_easydns.md index 12f69e09c..f4c44164c 100644 --- a/docs/content/dns/zz_gen_easydns.md +++ b/docs/content/dns/zz_gen_easydns.md @@ -28,7 +28,7 @@ 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 +lego --email you@example.com --dns easydns -d '*.example.com' -d example.com run ``` @@ -50,11 +50,11 @@ 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" %}}). 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..3ba5fffea 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 -d '*.example.com' -d example.com run ``` @@ -55,10 +55,9 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). @@ -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..cfdfb9bba 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 -d '*.example.com' -d example.com run ``` @@ -53,10 +53,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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. diff --git a/docs/content/dns/zz_gen_epik.md b/docs/content/dns/zz_gen_epik.md index a7fc029d3..861efb640 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). 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..f2f5f9619 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 -d '*.example.com' -d example.com 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,7 @@ 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 --d my.example.org run ``` It will then call the program './update-dns.sh' with like this: @@ -81,7 +81,7 @@ 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 -d 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..ffd3da1e4 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 -d '*.example.com' -d example.com run ``` @@ -50,10 +50,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | Environment Variable Name | Description | |--------------------------------|-------------| | `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" %}}). 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..421361205 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 -d '*.example.com' -d example.com run ``` @@ -47,11 +47,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_gandi.md b/docs/content/dns/zz_gen_gandi.md index b02d97819..fa7ae6fe0 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_gandiv5.md b/docs/content/dns/zz_gen_gandiv5.md index 78824abbe..c3f0e2d20 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 -d '*.example.com' -d example.com run ``` @@ -48,10 +48,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_gcloud.md b/docs/content/dns/zz_gen_gcloud.md index 64acc1d1e..556bffe3d 100644 --- a/docs/content/dns/zz_gen_gcloud.md +++ b/docs/content/dns/zz_gen_gcloud.md @@ -26,21 +26,9 @@ 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 +lego --email you@email.com --dns gcloud -d '*.example.com' -d example.com run ``` @@ -64,20 +52,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" %}}). -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..7dbb3cec8 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). 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..e49209d85 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 -d '*.example.com' -d example.com run ``` @@ -49,10 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_godaddy.md b/docs/content/dns/zz_gen_godaddy.md index bc51cd69b..9852a00d0 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 -d '*.example.com' -d example.com run ``` @@ -49,10 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_googledomains.md b/docs/content/dns/zz_gen_googledomains.md index 2421184c0..a7ccb031e 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 -d '*.example.com' -d example.com run ``` @@ -47,9 +47,9 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). 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..1e28e4445 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 -d '*.example.com' -d example.com run ``` @@ -37,7 +37,7 @@ 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" %}}). @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). @@ -60,7 +60,7 @@ 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..b2e575c4c 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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. 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..e2881c4fa 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 -d '*.example.com' -d example.com run ``` @@ -48,10 +48,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_httpnet.md b/docs/content/dns/zz_gen_httpnet.md index 862909697..8e333992f 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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. diff --git a/docs/content/dns/zz_gen_httpreq.md b/docs/content/dns/zz_gen_httpreq.md index 7f6a8d576..81a761d4c 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 -d '*.example.com' -d example.com run ``` @@ -48,10 +48,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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. diff --git a/docs/content/dns/zz_gen_huaweicloud.md b/docs/content/dns/zz_gen_huaweicloud.md index 46d121265..d5911eff6 100644 --- a/docs/content/dns/zz_gen_huaweicloud.md +++ b/docs/content/dns/zz_gen_huaweicloud.md @@ -29,7 +29,7 @@ Here is an example bash command using the Huawei Cloud provider: 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 +lego --email you@example.com --dns huaweicloud -d '*.example.com' -d example.com run ``` @@ -51,10 +51,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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) | +| `HUAWEICLOUD_HTTP_TIMEOUT` | API request timeout | +| `HUAWEICLOUD_POLLING_INTERVAL` | Time between DNS propagation check | +| `HUAWEICLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `HUAWEICLOUD_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" %}}). diff --git a/docs/content/dns/zz_gen_hurricane.md b/docs/content/dns/zz_gen_hurricane.md index 0c195d19c..385e6501b 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 -d '*.example.com' -d example.com 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 -d my.example.org -d demo.example.org ``` @@ -50,10 +50,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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) | +| `HURRICANE_HTTP_TIMEOUT` | API request timeout | +| `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 | The 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_hyperone.md b/docs/content/dns/zz_gen_hyperone.md index bc496f7bc..b533de5d5 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 -d '*.example.com' -d example.com run ``` @@ -39,12 +39,11 @@ 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" %}}). diff --git a/docs/content/dns/zz_gen_ibmcloud.md b/docs/content/dns/zz_gen_ibmcloud.md index c5a48d2ad..365377d2b 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 -d '*.example.com' -d example.com run ``` @@ -39,7 +39,7 @@ 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" %}}). @@ -49,10 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_iij.md b/docs/content/dns/zz_gen_iij.md index c7acfe3a0..b5e458db2 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 -d '*.example.com' -d example.com run ``` @@ -51,9 +51,9 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_iijdpf.md b/docs/content/dns/zz_gen_iijdpf.md index 12e126f49..b9635ac06 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 -d '*.example.com' -d example.com run ``` @@ -50,9 +50,9 @@ 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" %}}). diff --git a/docs/content/dns/zz_gen_infoblox.md b/docs/content/dns/zz_gen_infoblox.md index 74b80b2d1..ba7af4855 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 -d '*.example.com' -d example.com run ``` @@ -51,15 +51,14 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_infomaniak.md b/docs/content/dns/zz_gen_infomaniak.md index 7254241b1..4b737d4af 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 -d '*.example.com' -d example.com run ``` @@ -48,10 +48,10 @@ 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" %}}). diff --git a/docs/content/dns/zz_gen_internetbs.md b/docs/content/dns/zz_gen_internetbs.md index f0d9df3c1..3725bcb07 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 -d '*.example.com' -d example.com run ``` @@ -49,10 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_inwx.md b/docs/content/dns/zz_gen_inwx.md index 3e7d999e9..b51d58c07 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 -d '*.example.com' -d example.com 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 -d '*.example.com' -d example.com run ``` @@ -55,11 +55,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_ionos.md b/docs/content/dns/zz_gen_ionos.md index 78bd3ffb1..54d694da0 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). 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..6d7bcd24c 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 -d '*.example.com' -d example.com run ``` @@ -47,9 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). 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..8146a36ed 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 -d '*.example.com' -d example.com run ``` @@ -51,10 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). 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..2c0a6eafc 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 -d '*.example.com' -d example.com 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 -d '*.example.com' -d example.com 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 -d '*.example.com' -d example.com run ``` @@ -63,11 +63,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). 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..23bde4d79 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 you@example.com --dns liara -d '*.example.com' -d example.com run ``` @@ -47,11 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). @@ -61,7 +60,7 @@ 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..f2bbaefb7 100644 --- a/docs/content/dns/zz_gen_lightsail.md +++ b/docs/content/dns/zz_gen_lightsail.md @@ -47,8 +47,8 @@ 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" %}}). diff --git a/docs/content/dns/zz_gen_limacity.md b/docs/content/dns/zz_gen_limacity.md index 29bc6e0a7..fdaae55e6 100644 --- a/docs/content/dns/zz_gen_limacity.md +++ b/docs/content/dns/zz_gen_limacity.md @@ -27,7 +27,7 @@ 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 +lego --email you@example.com --dns limacity -d '*.example.com' -d example.com run ``` @@ -47,11 +47,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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) | +| `LIMACITY_HTTP_TIMEOUT` | API request timeout | +| `LIMACITY_POLLING_INTERVAL` | Time between DNS propagation check | +| `LIMACITY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `LIMACITY_SEQUENCE_INTERVAL` | Time between sequential requests | +| `LIMACITY_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" %}}). diff --git a/docs/content/dns/zz_gen_linode.md b/docs/content/dns/zz_gen_linode.md index e41ba7cd9..8b97123b2 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_liquidweb.md b/docs/content/dns/zz_gen_liquidweb.md index bd2ce63b6..511ba9c92 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 -d '*.example.com' -d example.com run ``` @@ -49,10 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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 | diff --git a/docs/content/dns/zz_gen_loopia.md b/docs/content/dns/zz_gen_loopia.md index bb3120c00..79827d325 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 you@example.com --dns loopia -d '*.example.com' -d example.com run ``` @@ -50,10 +50,10 @@ 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" %}}). diff --git a/docs/content/dns/zz_gen_luadns.md b/docs/content/dns/zz_gen_luadns.md index 8bf718ba3..2a6a02dd9 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 -d '*.example.com' -d example.com run ``` @@ -49,10 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_mailinabox.md b/docs/content/dns/zz_gen_mailinabox.md index 62a6bdb57..f3269620f 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 -d '*.example.com' -d example.com run ``` @@ -51,9 +51,8 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_manageengine.md b/docs/content/dns/zz_gen_manageengine.md index a39db8208..32266f2d2 100644 --- a/docs/content/dns/zz_gen_manageengine.md +++ b/docs/content/dns/zz_gen_manageengine.md @@ -28,7 +28,7 @@ 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 +lego --email you@example.com --dns manageengine -d '*.example.com' -d example.com run ``` @@ -49,9 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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) | +| `MANAGEENGINE_HTTP_TIMEOUT` | API request timeout | +| `MANAGEENGINE_POLLING_INTERVAL` | Time between DNS propagation check | +| `MANAGEENGINE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `MANAGEENGINE_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" %}}). 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..ea794d4e5 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 -d '*.example.com' -d example.com run ``` @@ -49,9 +49,9 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). 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 index 3d8f71aff..65c1d953d 100644 --- a/docs/content/dns/zz_gen_mijnhost.md +++ b/docs/content/dns/zz_gen_mijnhost.md @@ -27,7 +27,7 @@ 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 +lego --email you@example.com --dns mijnhost -d '*.example.com' -d example.com run ``` @@ -47,11 +47,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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) | +| `MIJNHOST_HTTP_TIMEOUT` | API request timeout | +| `MIJNHOST_POLLING_INTERVAL` | Time between DNS propagation check | +| `MIJNHOST_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `MIJNHOST_SEQUENCE_INTERVAL` | Time between sequential requests | +| `MIJNHOST_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" %}}). diff --git a/docs/content/dns/zz_gen_mittwald.md b/docs/content/dns/zz_gen_mittwald.md index 7714ef54f..c1edfe084 100644 --- a/docs/content/dns/zz_gen_mittwald.md +++ b/docs/content/dns/zz_gen_mittwald.md @@ -27,7 +27,7 @@ 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 +lego --email you@example.com --dns mittwald -d '*.example.com' -d example.com run ``` @@ -47,11 +47,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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) | +| `MITTWALD_HTTP_TIMEOUT` | API request timeout | +| `MITTWALD_POLLING_INTERVAL` | Time between DNS propagation check | +| `MITTWALD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `MITTWALD_SEQUENCE_INTERVAL` | Time between sequential requests | +| `MITTWALD_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" %}}). 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..4fc899bf0 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 -d '*.example.com' -d example.com run ``` @@ -49,9 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_mythicbeasts.md b/docs/content/dns/zz_gen_mythicbeasts.md index 70e38d249..86e2ae5fd 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 -d '*.example.com' -d example.com run ``` @@ -51,10 +51,10 @@ 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" %}}). diff --git a/docs/content/dns/zz_gen_namecheap.md b/docs/content/dns/zz_gen_namecheap.md index 9d7143d84..850a9ef8b 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 -d '*.example.com' -d example.com run ``` @@ -54,11 +54,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_namedotcom.md b/docs/content/dns/zz_gen_namedotcom.md index 2860ff0ae..df4c94559 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 -d '*.example.com' -d example.com run ``` @@ -49,10 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_namesilo.md b/docs/content/dns/zz_gen_namesilo.md index 207a1603f..1b69a3524 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 -d '*.example.com' -d example.com run ``` @@ -47,9 +47,9 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). 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..1649fd34c 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 -d '*.example.com' -d example.com run ``` @@ -49,11 +49,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). 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..e1973c814 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 -d '*.example.com' -d example.com run ``` @@ -51,9 +51,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_netlify.md b/docs/content/dns/zz_gen_netlify.md index 76651d9ef..ad41146dc 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_nicmanager.md b/docs/content/dns/zz_gen_nicmanager.md index a29d72120..1ae8806cc 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 -d '*.example.com' -d example.com 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 -d '*.example.com' -d example.com run ``` @@ -68,12 +68,12 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). @@ -81,7 +81,7 @@ 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..bd5d25321 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 -d '*.example.com' -d example.com run ``` @@ -49,10 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_njalla.md b/docs/content/dns/zz_gen_njalla.md index 9a312df8b..f846cf1e8 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_nodion.md b/docs/content/dns/zz_gen_nodion.md index 8d61eb834..fc1f820f8 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 you@example.com --dns nodion -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_ns1.md b/docs/content/dns/zz_gen_ns1.md index b2262169d..9e4c906ad 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). 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..1b6647ce5 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 -d '*.example.com' -d example.com run ``` @@ -51,12 +44,12 @@ 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" %}}). @@ -66,16 +59,9 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_otc.md b/docs/content/dns/zz_gen_otc.md index 9da69c694..0de59fd64 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,6 +35,7 @@ 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 | @@ -53,13 +48,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_ovh.md b/docs/content/dns/zz_gen_ovh.md index aaafded85..fad507cbd 100644 --- a/docs/content/dns/zz_gen_ovh.md +++ b/docs/content/dns/zz_gen_ovh.md @@ -32,20 +32,20 @@ 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 +lego --email you@example.com --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 -d '*.example.com' -d example.com 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 -d '*.example.com' -d example.com run ``` @@ -71,10 +71,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_pdns.md b/docs/content/dns/zz_gen_pdns.md index 7c2a8c663..31870fbc0 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 -d '*.example.com' -d example.com run ``` @@ -50,11 +50,11 @@ 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" %}}). diff --git a/docs/content/dns/zz_gen_plesk.md b/docs/content/dns/zz_gen_plesk.md index 73ec9a55d..5c9d060cf 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 -d '*.example.com' -d example.com run ``` @@ -51,10 +51,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_porkbun.md b/docs/content/dns/zz_gen_porkbun.md index f54e6f688..5e96e239e 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 -d '*.example.com' -d example.com run ``` @@ -49,10 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_rackspace.md b/docs/content/dns/zz_gen_rackspace.md index b9a2ab710..bbdd8cbfb 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 -d '*.example.com' -d example.com run ``` @@ -49,10 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_rainyun.md b/docs/content/dns/zz_gen_rainyun.md index 680eb845a..c0ff646b8 100644 --- a/docs/content/dns/zz_gen_rainyun.md +++ b/docs/content/dns/zz_gen_rainyun.md @@ -27,7 +27,7 @@ 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 +lego --email you@example.com --dns rainyun -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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) | +| `RAINYUN_HTTP_TIMEOUT` | API request timeout | +| `RAINYUN_POLLING_INTERVAL` | Time between DNS propagation check | +| `RAINYUN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `RAINYUN_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" %}}). diff --git a/docs/content/dns/zz_gen_rcodezero.md b/docs/content/dns/zz_gen_rcodezero.md index a544df420..8677de764 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_regfish.md b/docs/content/dns/zz_gen_regfish.md index 357ce0764..f5310db53 100644 --- a/docs/content/dns/zz_gen_regfish.md +++ b/docs/content/dns/zz_gen_regfish.md @@ -27,7 +27,7 @@ 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 +lego --email you@example.com --dns regfish -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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) | +| `REGFISH_HTTP_TIMEOUT` | API request timeout | +| `REGFISH_POLLING_INTERVAL` | Time between DNS propagation check | +| `REGFISH_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `REGFISH_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" %}}). diff --git a/docs/content/dns/zz_gen_regru.md b/docs/content/dns/zz_gen_regru.md index eaf163a13..8c6bea662 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 -d '*.example.com' -d example.com run ``` @@ -49,12 +49,12 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_rfc2136.md b/docs/content/dns/zz_gen_rfc2136.md index 1b1d43dd5..ad52005d4 100644 --- a/docs/content/dns/zz_gen_rfc2136.md +++ b/docs/content/dns/zz_gen_rfc2136.md @@ -30,7 +30,7 @@ RFC2136_NAMESERVER=127.0.0.1 \ RFC2136_TSIG_KEY=example.com \ 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 -d '*.example.com' -d example.com run ## --- @@ -38,7 +38,7 @@ keyname=example.com; keyfile=example.com.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 +lego --email you@example.com --dns rfc2136 -d '*.example.com' -d example.com run ``` @@ -61,12 +61,12 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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_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_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_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" %}}). diff --git a/docs/content/dns/zz_gen_rimuhosting.md b/docs/content/dns/zz_gen_rimuhosting.md index acb829e93..46687484c 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_route53.md b/docs/content/dns/zz_gen_route53.md index 59e489d6a..cd18a5c1d 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 --email you@example.com --dns route53 -d '*.example.com' -d example.com run ``` @@ -59,11 +59,10 @@ 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" %}}). diff --git a/docs/content/dns/zz_gen_safedns.md b/docs/content/dns/zz_gen_safedns.md index 4c20fca6a..c6d4cd745 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_sakuracloud.md b/docs/content/dns/zz_gen_sakuracloud.md index b43f83ef4..e0af53acf 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 -d '*.example.com' -d example.com run ``` @@ -49,10 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_scaleway.md b/docs/content/dns/zz_gen_scaleway.md index 4033a9bd6..111d18a42 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 -d '*.example.com' -d example.com run ``` @@ -49,10 +49,9 @@ 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" %}}). diff --git a/docs/content/dns/zz_gen_selectel.md b/docs/content/dns/zz_gen_selectel.md index d994d6633..00e5b5bad 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 -d '*.example.com' -d example.com run ``` @@ -48,10 +48,10 @@ 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" %}}). diff --git a/docs/content/dns/zz_gen_selectelv2.md b/docs/content/dns/zz_gen_selectelv2.md index 0873d810c..bb09241aa 100644 --- a/docs/content/dns/zz_gen_selectelv2.md +++ b/docs/content/dns/zz_gen_selectelv2.md @@ -30,7 +30,7 @@ SELECTELV2_USERNAME=trex \ SELECTELV2_PASSWORD=xxxxx \ SELECTELV2_ACCOUNT_ID=1234567 \ SELECTELV2_PROJECT_ID=111a11111aaa11aa1a11aaa11111aa1a \ -lego --dns selectelv2 -d '*.example.com' -d example.com run +lego --email you@example.com --dns selectelv2 -d '*.example.com' -d example.com run ``` @@ -53,14 +53,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_selfhostde.md b/docs/content/dns/zz_gen_selfhostde.md index 363f782e0..81abe85c1 100644 --- a/docs/content/dns/zz_gen_selfhostde.md +++ b/docs/content/dns/zz_gen_selfhostde.md @@ -29,7 +29,7 @@ Here is an example bash command using the SelfHost.(de|eu) provider: SELFHOSTDE_USERNAME=xxx \ SELFHOSTDE_PASSWORD=yyy \ SELFHOSTDE_RECORDS_MAPPING=my.example.com:123 \ -lego --dns selfhostde -d '*.example.com' -d example.com run +lego --email you@example.com --dns selfhostde -d '*.example.com' -d example.com run ``` @@ -51,10 +51,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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) | +| `SELFHOSTDE_HTTP_TIMEOUT` | API request timeout | +| `SELFHOSTDE_POLLING_INTERVAL` | Time between DNS propagation check | +| `SELFHOSTDE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `SELFHOSTDE_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" %}}). diff --git a/docs/content/dns/zz_gen_servercow.md b/docs/content/dns/zz_gen_servercow.md index 7d00a6306..ce47077df 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 -d '*.example.com' -d example.com run ``` @@ -49,10 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). @@ -62,7 +62,7 @@ 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..1719e07c9 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 -d '*.example.com' -d example.com run ``` @@ -49,10 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_simply.md b/docs/content/dns/zz_gen_simply.md index edfa14380..1603ee53f 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 -d '*.example.com' -d example.com run ``` @@ -49,10 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_sonic.md b/docs/content/dns/zz_gen_sonic.md index 20729bc1a..2adb435a9 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 -d '*.example.com' -d example.com run ``` @@ -49,11 +49,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). 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..cbafa4289 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 -d '*.example.com' -d example.com run ``` @@ -51,9 +51,9 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). 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 index ff7f2e6ed..ecfa204ce 100644 --- a/docs/content/dns/zz_gen_technitium.md +++ b/docs/content/dns/zz_gen_technitium.md @@ -28,7 +28,7 @@ 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 +lego --email you@example.com --dns technitium -d '*.example.com' -d example.com run ``` @@ -49,10 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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) | +| `TECHNITIUM_HTTP_TIMEOUT` | API request timeout | +| `TECHNITIUM_POLLING_INTERVAL` | Time between DNS propagation check | +| `TECHNITIUM_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `TECHNITIUM_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" %}}). diff --git a/docs/content/dns/zz_gen_tencentcloud.md b/docs/content/dns/zz_gen_tencentcloud.md index 178ffcf43..bc93c225e 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 -d '*.example.com' -d example.com run ``` @@ -49,12 +49,12 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_timewebcloud.md b/docs/content/dns/zz_gen_timewebcloud.md index 83d5b831b..e933043a4 100644 --- a/docs/content/dns/zz_gen_timewebcloud.md +++ b/docs/content/dns/zz_gen_timewebcloud.md @@ -27,7 +27,7 @@ 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 +lego --email you@example.com --dns timewebcloud -d '*.example.com' -d example.com run ``` @@ -47,9 +47,9 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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) | +| `TIMEWEBCLOUD_HTTP_TIMEOUT` | API request timeout | +| `TIMEWEBCLOUD_POLLING_INTERVAL` | Time between DNS propagation check | +| `TIMEWEBCLOUD_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" %}}). 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..64db62dc6 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 -d '*.example.com' -d example.com run ``` @@ -49,10 +49,9 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_ultradns.md b/docs/content/dns/zz_gen_ultradns.md index d6d89c77b..36a233ae2 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 -d '*.example.com' -d example.com run ``` @@ -50,9 +50,9 @@ 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" %}}). 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..5fc6dfea6 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 -d '*.example.com' -d example.com run ``` @@ -47,11 +47,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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) | +| `VARIOMEDIA_HTTP_TIMEOUT` | API request timeout | +| `VARIOMEDIA_POLLING_INTERVAL` | Time between DNS propagation check | +| `VARIOMEDIA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `VARIOMEDIA_SEQUENCE_INTERVAL` | Time between sequential requests | +| `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" %}}). diff --git a/docs/content/dns/zz_gen_vegadns.md b/docs/content/dns/zz_gen_vegadns.md index e06eebce7..b9fe43c1f 100644 --- a/docs/content/dns/zz_gen_vegadns.md +++ b/docs/content/dns/zz_gen_vegadns.md @@ -46,9 +46,9 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_vercel.md b/docs/content/dns/zz_gen_vercel.md index 71f2eeed5..e092b4fff 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 -d '*.example.com' -d example.com run ``` @@ -47,11 +47,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_versio.md b/docs/content/dns/zz_gen_versio.md index 5d2cc0118..3941605c4 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 -d '*.example.com' -d example.com run ``` @@ -50,11 +50,11 @@ 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" %}}). diff --git a/docs/content/dns/zz_gen_vinyldns.md b/docs/content/dns/zz_gen_vinyldns.md index 3280d6f0a..92e0138dd 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 -d '*.example.com' -d example.com run ``` @@ -51,11 +51,9 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). 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..d3c33e9c2 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 -d '*.example.com' -d example.com run ``` @@ -54,9 +54,9 @@ 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" %}}). diff --git a/docs/content/dns/zz_gen_volcengine.md b/docs/content/dns/zz_gen_volcengine.md index 587ce1e74..a1eb5d4ec 100644 --- a/docs/content/dns/zz_gen_volcengine.md +++ b/docs/content/dns/zz_gen_volcengine.md @@ -28,7 +28,7 @@ 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 +lego --email you@example.com --dns volcengine -d '*.example.com' -d example.com run ``` @@ -50,12 +50,12 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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_HTTP_TIMEOUT` | API request timeout | +| `VOLC_POLLING_INTERVAL` | Time between DNS propagation check | +| `VOLC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `VOLC_REGION` | Region | | `VOLC_SCHEME` | API scheme | -| `VOLC_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) | +| `VOLC_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" %}}). diff --git a/docs/content/dns/zz_gen_vscale.md b/docs/content/dns/zz_gen_vscale.md index c33e2f7b5..696d404d8 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 -d '*.example.com' -d example.com run ``` @@ -48,10 +48,10 @@ 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" %}}). diff --git a/docs/content/dns/zz_gen_vultr.md b/docs/content/dns/zz_gen_vultr.md index 4160fbcf3..0334a69ad 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_webnames.md b/docs/content/dns/zz_gen_webnames.md index cad02c287..2fdc09cd3 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 -d '*.example.com' -d example.com run ``` @@ -37,7 +37,7 @@ 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" %}}). @@ -47,9 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). 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..c48181a54 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 you@example.com --dns websupport -d '*.example.com' -d example.com run ``` @@ -49,11 +49,11 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). @@ -63,7 +63,7 @@ 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..1762cf4ca 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 -d '*.example.com' -d example.com run ``` @@ -49,10 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_westcn.md b/docs/content/dns/zz_gen_westcn.md index a5523b955..fdda3b246 100644 --- a/docs/content/dns/zz_gen_westcn.md +++ b/docs/content/dns/zz_gen_westcn.md @@ -28,7 +28,7 @@ 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 +lego --email you@example.com --dns westcn -d '*.example.com' -d example.com run ``` @@ -49,10 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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) | +| `WESTCN_HTTP_TIMEOUT` | API request timeout | +| `WESTCN_POLLING_INTERVAL` | Time between DNS propagation check | +| `WESTCN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `WESTCN_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" %}}). diff --git a/docs/content/dns/zz_gen_yandex.md b/docs/content/dns/zz_gen_yandex.md index 4a1cf1f99..60b8a0ac3 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_yandex360.md b/docs/content/dns/zz_gen_yandex360.md index d831fdfc2..04eeab45c 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 -d '*.example.com' -d example.com run ``` @@ -49,10 +49,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/dns/zz_gen_yandexcloud.md b/docs/content/dns/zz_gen_yandexcloud.md index 0564e93d2..0831e8c49 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 -d '*.example.com' -d example.com 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 -d '*.example.com' -d example.com run ``` @@ -62,9 +62,9 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). 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..a6df03b56 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 -d '*.example.com' -d example.com run ``` @@ -50,9 +50,10 @@ 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" %}}). diff --git a/docs/content/dns/zz_gen_zonomi.md b/docs/content/dns/zz_gen_zonomi.md index fd8757f82..51c25d95d 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 -d '*.example.com' -d example.com run ``` @@ -47,10 +47,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | 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" %}}). diff --git a/docs/content/usage/cli/Options.md b/docs/content/usage/cli/Options.md index 7b5df027a..a6484de23 100644 --- a/docs/content/usage/cli/Options.md +++ b/docs/content/usage/cli/Options.md @@ -142,32 +142,3 @@ 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/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index 139143b17..84615c54d 100644 --- a/docs/data/zz_cli_help.toml +++ b/docs/data/zz_cli_help.toml @@ -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,14 +32,12 @@ 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) @@ -74,12 +71,9 @@ OPTIONS: --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 """ @@ -94,8 +88,7 @@ USAGE: 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-disable Do not 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,10 +96,8 @@ 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 @@ -152,7 +143,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, corenetworks, cpanel, derak, desec, designate, digitalocean, directadmin, 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, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, mijnhost, mittwald, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, netcup, netlify, nicmanager, nifcloud, njalla, nodion, ns1, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, stackpath, technitium, tencentcloud, timewebcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, volcengine, vscale, vultr, webnames, websupport, wedos, westcn, 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..5cb2add45 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/McShelby/hugo-theme-relearn v0.0.0-20240802145348-259f21f89851 diff --git a/docs/go.sum b/docs/go.sum index b62d5c809..1ed963e87 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/McShelby/hugo-theme-relearn v0.0.0-20240802145348-259f21f89851 h1:JpmKIb1bRzuAcgnphwSb35Xz9rk/Alq19uRWVGSwScA= +github.com/McShelby/hugo-theme-relearn v0.0.0-20240802145348-259f21f89851/go.mod h1:mKQQdxZNIlLvAj8X3tMq+RzntIJSr9z7XdzuMomt0IM= diff --git a/docs/hugo.toml b/docs/hugo.toml index fe076a306..a974cea73 100644 --- a/docs/hugo.toml +++ b/docs/hugo.toml @@ -2,20 +2,47 @@ 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 = "" # 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] @@ -44,7 +71,7 @@ title = "Lego" weight = 12 [outputs] - home = ['html', 'rss', 'print'] + home = [ "html", "rss", "search", "searchpage"] [module] [[module.imports]] diff --git a/docs/static/.nojekyll b/docs/static/.nojekyll deleted file mode 100644 index e69de29bb..000000000 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..605a77bd0 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) @@ -102,10 +99,9 @@ func TestChallengeDNS_Client_Obtain(t *testing.T) { 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..fcd88001a 100644 --- a/go.mod +++ b/go.mod @@ -1,229 +1,209 @@ module github.com/go-acme/lego/v4 -go 1.24.0 +go 1.22.0 require ( - cloud.google.com/go/compute/metadata v0.9.0 + cloud.google.com/go/compute/metadata v0.6.0 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.16.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 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/resourcegraph/armresourcegraph v0.9.0 - github.com/Azure/go-autorest/autorest v0.11.30 + github.com/Azure/go-autorest/autorest v0.11.29 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/Azure/go-autorest/autorest/to v0.4.0 + github.com/BurntSushi/toml v1.4.0 + 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.63.72 + github.com/aws/aws-sdk-go-v2 v1.32.7 + github.com/aws/aws-sdk-go-v2/config v1.28.7 + github.com/aws/aws-sdk-go-v2/credentials v1.17.48 + github.com/aws/aws-sdk-go-v2/service/lightsail v1.42.8 + github.com/aws/aws-sdk-go-v2/service/route53 v1.46.4 + github.com/aws/aws-sdk-go-v2/service/s3 v1.71.1 + github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 + github.com/cenkalti/backoff/v4 v4.3.0 + github.com/civo/civogo v0.3.11 + github.com/cloudflare/cloudflare-go v0.112.0 + github.com/cpu/goacmedns v0.1.1 + github.com/dnsimple/dnsimple-go v1.7.0 + github.com/exoscale/egoscale/v3 v3.1.7 + github.com/go-jose/go-jose/v4 v4.0.4 + github.com/go-viper/mapstructure/v2 v2.2.1 + github.com/google/go-querystring v1.1.0 github.com/gophercloud/gophercloud v1.14.1 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.7 + github.com/hashicorp/go-version v1.7.0 + github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128 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.44.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.62 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.10.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/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/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.81.1 + github.com/ovh/go-ovh v1.6.0 + 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/sacloud/api-client-go v0.2.10 + github.com/sacloud/iaas-api-go v1.14.0 + github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 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/selectel/go-selvpcclient/v3 v3.2.1 + github.com/softlayer/softlayer-go v1.1.7 + github.com/stretchr/testify v1.10.0 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065 + github.com/transip/gotransip/v6 v6.26.0 + github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec + github.com/urfave/cli/v2 v2.27.5 + github.com/vinyldns/go-vinyldns v0.9.16 + github.com/volcengine/volc-sdk-golang v1.0.189 + github.com/vultr/govultr/v3 v3.9.1 + github.com/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c + github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134 + golang.org/x/crypto v0.31.0 + golang.org/x/net v0.33.0 + golang.org/x/oauth2 v0.24.0 + golang.org/x/text v0.21.0 + golang.org/x/time v0.8.0 + google.golang.org/api v0.214.0 + gopkg.in/ns1/ns1-go.v2 v2.13.0 gopkg.in/yaml.v2 v2.4.0 - software.sslmate.com/src/go-pkcs12 v0.7.0 + software.sslmate.com/src/go-pkcs12 v0.5.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/auth v0.13.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.6 // 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.10.0 // 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/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.7 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.26 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.8 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 // indirect + github.com/aws/smithy-go v1.22.1 // 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/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // 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.8.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // 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.2 // 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/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/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/go-playground/validator/v10 v10.16.0 // indirect + github.com/go-resty/resty/v2 v2.16.2 // indirect + github.com/goccy/go-json v0.10.4 // indirect + github.com/gofrs/flock v0.12.1 // indirect + github.com/golang-jwt/jwt/v4 v4.5.1 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/google/s2a-go v0.1.8 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/googleapis/gax-go/v2 v2.14.0 // 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/leodido/go-urn v1.2.4 // 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/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/peterhellberg/link v1.2.0 // 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/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/sacloud/go-http v0.1.8 // indirect + github.com/sacloud/packages-go v0.0.10 // 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/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/sony/gobreaker v0.5.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/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.18.2 // 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 + go.mongodb.org/mongo-driver v1.12.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + go.uber.org/ratelimit v0.3.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 + golang.org/x/mod v0.22.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/tools v0.28.0 // indirect + google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect + google.golang.org/grpc v1.67.1 // indirect + google.golang.org/protobuf v1.35.2 // 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..c5e736459 100644 --- a/go.sum +++ b/go.sum @@ -13,18 +13,18 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV 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/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs= +cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q= +cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU= +cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= 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/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= 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= @@ -42,14 +42,14 @@ github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYs 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.16.0 h1:JZg6HRh6W6U4OLl6lk7BZ7BLisIzM9dG1R50zUk9C/M= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0/go.mod h1:YL1xnZ6QejvQHWJrX/AvhFl4WW4rqHVoKspWNVwFk0M= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvULkDNfdXOgrjtg6UYJPFBJyuEcRCAw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0/go.mod h1:PwOyop78lveYMRs6oCxjiVyBdyCgIYH6XHIVZO9/SFQ= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= 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= @@ -63,8 +63,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1. 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.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= @@ -77,25 +77,28 @@ github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSY 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.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 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/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/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= @@ -103,125 +106,69 @@ github.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae/go.mod h1:/ 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/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.63.72 h1:HvFZUzEbNvfe8F2Mg0wBGv90bPhWDxgVtDHR5zoBOU0= +github.com/aliyun/alibaba-cloud-sdk-go v1.63.72/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 v1.32.7 h1:ky5o35oENWi0JYWUZkB7WYvVPP+bcRF5/Iq7JWSb5Rw= +github.com/aws/aws-sdk-go-v2 v1.32.7/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc= +github.com/aws/aws-sdk-go-v2/config v1.28.7 h1:GduUnoTXlhkgnxTD93g1nv4tVPILbdNQOzav+Wpg7AE= +github.com/aws/aws-sdk-go-v2/config v1.28.7/go.mod h1:vZGX6GVkIE8uECSUHB6MWAUsd4ZcG2Yq/dMa4refR3M= +github.com/aws/aws-sdk-go-v2/credentials v1.17.48 h1:IYdLD1qTJ0zanRavulofmqut4afs45mOWEI+MzZtTfQ= +github.com/aws/aws-sdk-go-v2/credentials v1.17.48/go.mod h1:tOscxHN3CGmuX9idQ3+qbkzrjVIx32lqDSU1/0d/qXs= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22 h1:kqOrpojG71DxJm/KDPO+Z/y1phm1JlC8/iT+5XRmAn8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22/go.mod h1:NtSFajXVVL8TA2QNngagVZmUtXciyrHOt7xgz4faS/M= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 h1:I/5wmGMffY4happ8NOCuIUEWGUvvFp5NSeQcXl9RHcI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26/go.mod h1:FR8f4turZtNy6baO0KJ5FJUmXH/cSkI9fOngs0yl6mA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 h1:zXFLuEuMMUOvEARXFUVJdfqZ4bvvSgdGRq/ATcrQxzM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26/go.mod h1:3o2Wpy0bogG1kyOPrgkXA8pgIfEEv0+m19O9D5+W8y8= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.26 h1:GeNJsIFHB+WW5ap2Tec4K6dzcVTsRbsT1Lra46Hv9ME= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.26/go.mod h1:zfgMpwHDXX2WGoG84xG2H+ZlPTkJUU4YUvx2svLQYWo= 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/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.7 h1:tB4tNw83KcajNAzaIMhkhVI2Nt8fAZd5A5ro113FEMY= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.7/go.mod h1:lvpyBGkZ3tZ9iSsUIcC2EWp+0ywa7aK3BLT+FwZi+mQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 h1:8eUsivBQzZHqe/3FE+cqwfH+0p5Jo8PFM/QYQSmeZ+M= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7/go.mod h1:kLPQvGUmxn/fqiCrDeohwG33bq2pQpGeY62yRO6Nrh0= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.7 h1:Hi0KGbrnr57bEHWM0bJ1QcBzxLrL/k2DHvGYhb8+W1w= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.7/go.mod h1:wKNgWgExdjjrm4qvfbTorkvocEstaoDl4WCvGfeCy9c= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.42.8 h1:+lmJoqxuUoPlSfGk5JYQQivd9YFjUvRZR6RPY+Wcx48= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.42.8/go.mod h1:Gg8/myP4+rgRi4+j9gQdbOEnMtwMAUUIeXo+nKCFVj8= +github.com/aws/aws-sdk-go-v2/service/route53 v1.46.4 h1:0jMtawybbfpFEIMy4wvfyW2Z4YLr7mnuzT0fhR67Nrc= +github.com/aws/aws-sdk-go-v2/service/route53 v1.46.4/go.mod h1:xlMODgumb0Pp8bzfpojqelDrf8SL9rb5ovwmwKJl+oU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.71.1 h1:aOVVZJgWbaH+EJYPvEgkNhCEbXXvH7+oML36oaPK3zE= +github.com/aws/aws-sdk-go-v2/service/s3 v1.71.1/go.mod h1:r+xl5yzMk9083rMR+sJ5TYj9Tihvf/l1oxzZXDgGj2Q= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.8 h1:CvuUmnXI7ebaUAhbJcDy9YQx8wHR69eZ9I7q5hszt/g= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.8/go.mod h1:XDeGv1opzwm8ubxddF0cgqkZWsyOtw4lr6dxwmb6YQg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 h1:F2rBfNAL5UyswqoeWv9zs74N/NanhK16ydHW1pahX6E= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7/go.mod h1:JfyQ0g2JG8+Krq0EuZNnRwX0mU0HrwY/tG6JNfcqh4k= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 h1:Xgv/hyNgvLda/M9l9qxXc4UFSgppnRczLxlMs5Ae/QY= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.3/go.mod h1:5Gn+d+VaaRgsjewpMvGazt0WfcFO+Md4wLOuBfGR9Bc= 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/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= +github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= 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/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= @@ -235,9 +182,8 @@ github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInq github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= 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 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= 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= @@ -248,10 +194,12 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P 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/civo/civogo v0.3.11 h1:mON/fyrV946Sbk6paRtOSGsN+asCgCmHCgArf5xmGxM= +github.com/civo/civogo v0.3.11/go.mod h1:7+GeeFwc4AYTULaEshpT2vIcl3Qq8HPoxA17viX3l6g= 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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/cloudflare-go v0.112.0 h1:caFwqXdGJCl3rjVMgbPEn8iCYAg9JsRYV3dIVQE5d7g= +github.com/cloudflare/cloudflare-go v0.112.0/go.mod h1:QB55kuJ5ZTeLNFcLJePfMuBilhu/LDKpLBmKFQIoSZ0= 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= @@ -261,21 +209,25 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee 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/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-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 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.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 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/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/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= @@ -289,11 +241,10 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m 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/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/v3 v3.1.7 h1:Q6p9tOVY0IiOW0fUpaPQWY7ggGEuSPZLAGxFgDd2sCE= +github.com/exoscale/egoscale/v3 v3.1.7/go.mod h1:GHKucK/J26v8PGWztGdhxWNMjrjG9PbelxKCJ4YI11Q= 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= @@ -311,30 +262,20 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z 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.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 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/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.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= +github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= 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= @@ -345,53 +286,46 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V 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.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/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-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/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/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.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= +github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg= +github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= 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-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= 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/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= +github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 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/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/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= 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/v4 v4.0.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/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/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= +github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +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= @@ -422,8 +356,6 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 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= @@ -443,14 +375,12 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ 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= @@ -461,29 +391,28 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf 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 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= 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.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= +github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= +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.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= 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.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPqMNIe8o= +github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk= 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/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= @@ -495,10 +424,13 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmg 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= @@ -513,9 +445,11 @@ github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjh 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-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.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-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= 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= @@ -525,8 +459,8 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b 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-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.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= @@ -541,8 +475,8 @@ github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn 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/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128 h1:kQ2Agpfy7Ze1ajn9xCQG9G6T7XIbqv+FBDS/U98W9Mk= +github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128/go.mod h1:JWz2ujO9X3oU5wb6kXp+DpR2UuDj2SldDbX8T0FSuhI= 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= @@ -550,18 +484,20 @@ github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df h1:MZf03xP9WdakyXhOWuAD5 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/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= +github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= 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/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= @@ -572,9 +508,8 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u 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= @@ -583,13 +518,14 @@ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8 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/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= 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/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= 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= @@ -611,13 +547,10 @@ 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/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.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/linode/linodego v1.44.0 h1:JZLLWzCAx3CmHSV9NmCoXisuqKtrmPhfY9MrgvaHMUY= +github.com/linode/linodego v1.44.0/go.mod h1:umdoNOmtbqAdGQbmQnPFZ2YS4US+/mU/1bA7MjoKAvg= 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= @@ -640,7 +573,6 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME 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= @@ -653,8 +585,8 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N 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.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= +github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= 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= @@ -685,8 +617,8 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY 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/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/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= @@ -694,35 +626,28 @@ github.com/nats-io/nats.go v1.12.1/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/ 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/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.10.0 h1:qrEDiqnsvNU9QE7lXIXi/tIHAfyaFXKxF2/8/52O8uM= +github.com/nrdcg/desec v0.10.0/go.mod h1:5+4vyhMRTs49V9CNoODF/HwT8Mwxv9DJ6j+7NekUnBs= 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/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/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= @@ -737,23 +662,30 @@ 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/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= +github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= 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/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo= +github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0= 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/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/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/oracle/oci-go-sdk/v65 v65.81.1 h1:JYc47bk8n/MUchA2KHu1ggsCQzlJZQLJ+tTKfOho00E= +github.com/oracle/oci-go-sdk/v65 v65.81.1/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0= +github.com/ovh/go-ovh v1.6.0 h1:ixLOwxQdzYDx296sXcgS35TOPEahJkpjMGtzPadCjQI= +github.com/ovh/go-ovh v1.6.0/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= @@ -777,8 +709,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI 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= @@ -811,38 +743,40 @@ github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2 h1:dq90+d51/hQR 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/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= +github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= 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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 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/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.14.0 h1:xjkFWqdo4ilTrKPNNYBNWR/CZ/kVRsJrdAHAad6J/AQ= +github.com/sacloud/iaas-api-go v1.14.0/go.mod h1:C8os2Mnj0TOmMdSllwhaDWKMVG2ysFnpe69kyA4M3V0= +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/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/scaleway/scaleway-sdk-go v1.0.0-beta.30 h1:yoKAVkEVwAqbGbR8n87rHQ1dulL25rKloGadb3vm770= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30/go.mod h1:sH0u6fq6x4R5M7WxkoQFY/o7UaiItec0o1LinLCJNq8= 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/go-selvpcclient/v3 v3.2.1 h1:ny6WIAMiHzKxOgOEnwcWE79wIQij1AHHylzPA41MXCw= +github.com/selectel/go-selvpcclient/v3 v3.2.1/go.mod h1:3EfSf8aEWyhspOGbvZ6mvnFg7JN5uckxNyBFPGWsXNQ= +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= @@ -851,16 +785,21 @@ github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic 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.7 h1:SgTL+pQZt1h+5QkAhVmHORM/7N9c1X0sljJhuOIHxWE= +github.com/softlayer/softlayer-go v1.1.7/go.mod h1:WeJrBLoTJcaT8nO1azeyHyNpo/fDLtbpbvh+pzts+Qw= 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/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= +github.com/sony/gobreaker v0.5.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/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -870,15 +809,14 @@ 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/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/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 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= @@ -888,7 +826,6 @@ github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1Sd 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= @@ -903,53 +840,54 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ 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.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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.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/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065 h1:krcqtAmexnHHBm/4ge4tr2b1cn/a7JGBESVGoZYXQAE= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065 h1:aEFtLD1ceyeljQXB1S2BjN0zjTkf0X3XmpuxFIiC29w= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065/go.mod h1:HWvwy09hFSMXrj9SMvVRWV4U7rZO3l+WuogyNuxiT3M= 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/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/transip/gotransip/v6 v6.26.0 h1:Aejfvh8rSp8Mj2GX/RpdBjMCv+Iy/DmgfNgczPDP550= +github.com/transip/gotransip/v6 v6.26.0/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/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/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec h1:2s/ghQ8wKE+UzD/hf3P4Gd1j0JI9ncbxv+nsypPoUYI= +github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec/go.mod h1:BZr7Qs3ku1ckpqed8tCRSqTlp8NAeZfAVpfx4OzXMss= 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/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +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/volcengine/volc-sdk-golang v1.0.189 h1:VMDTHWYXakXJtZqPYn0As/h4eB0c4imvyru6mIp+o60= +github.com/volcengine/volc-sdk-golang v1.0.189/go.mod h1:u0VtPvlXWpXDTmc9IHkaW1q+5Jjwus4oAqRhNMDRInE= +github.com/vultr/govultr/v3 v3.9.1 h1:uxSIb8Miel7tqTs3ee+z3t+JelZikwqBBsZzCOPBy/8= +github.com/vultr/govultr/v3 v3.9.1/go.mod h1:Rd8ebpXm7jxH3MDmhnEs+zrlYW212ouhx+HeUMfHm2o= 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/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/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c h1:Rnr+lDYXVkP+3eT8/d68iq4G/UeIhyCQk+HKa8toTvg= +github.com/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo= +github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134 h1:qmpz0Kvr9GAng8LAhRcKIpY71CEAcL3EBkftVlsP5Cw= +github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134/go.mod h1:KgZCJrxdhdw/sKhTQ/M3S9WOLri2PCnBlc4C3s+PfKY= 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/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -959,50 +897,38 @@ go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQc 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.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE= +go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0= 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.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= 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/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +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= @@ -1011,9 +937,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U 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= @@ -1026,17 +950,13 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/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.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 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.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.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 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= @@ -1077,11 +997,8 @@ golang.org/x/mod v0.3.0/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/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/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 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= @@ -1127,28 +1044,22 @@ golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qx 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-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.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 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.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 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= @@ -1162,11 +1073,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ 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/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/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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= @@ -1208,7 +1116,6 @@ golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7w 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= @@ -1233,43 +1140,30 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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= @@ -1284,20 +1178,18 @@ 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/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/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 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.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.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.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= @@ -1338,7 +1230,6 @@ golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjs 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= @@ -1353,20 +1244,14 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 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.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= +golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= 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= @@ -1385,8 +1270,8 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M 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.214.0 h1:h2Gkq07OYi6kusGOaT/9rnNljuXmqPnaig7WGPmKbwA= +google.golang.org/api v0.214.0/go.mod h1:bYPpLG8AyeMWwDU6NXoB00xC0DFkikVvd5MfwoxjLqE= 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= @@ -1425,12 +1310,12 @@ google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6D 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-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU= +google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38/go.mod h1:xBI+tzfqGGN2JBeSebfKXFSdBpWVQ7sLW40PTupVRm4= +google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ= +google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= 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= @@ -1448,8 +1333,8 @@ google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv 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.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= 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= @@ -1464,8 +1349,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 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.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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= @@ -1476,15 +1361,16 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV 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.13.0 h1:I5NNqI9Bi1SGK92TVkOvLTwux5LNrix/99H2datVh48= +gopkg.in/ns1/ns1-go.v2 v2.13.0/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= @@ -1501,6 +1387,7 @@ 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= @@ -1515,5 +1402,5 @@ 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.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M= +software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/internal/clihelp/generator.go b/internal/clihelp/generator.go index fcabde015..2d256b4d7 100644 --- a/internal/clihelp/generator.go +++ b/internal/clihelp/generator.go @@ -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/generator.go b/internal/dns/docs/generator.go index 9355d0d1b..a6b91b45d 100644 --- a/internal/dns/docs/generator.go +++ b/internal/dns/docs/generator.go @@ -48,11 +48,6 @@ func main() { log.Fatal(err) } - err = cleanDocumentation() - if err != nil { - log.Fatal(err) - } - for _, m := range models.Providers { // generate documentation err = generateDocumentation(m) @@ -76,22 +71,6 @@ func main() { 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") @@ -116,9 +95,8 @@ func generateCLIHelp(models *descriptors.Providers) error { defer func() { _ = file.Close() }() b := &bytes.Buffer{} - err = template.Must( - template.New(filepath.Base(cliTemplate)).Funcs(map[string]any{ + template.New(filepath.Base(cliTemplate)).Funcs(map[string]interface{}{ "safe": func(src string) string { return strings.ReplaceAll(src, "`", "'") }, @@ -135,7 +113,6 @@ func generateCLIHelp(models *descriptors.Providers) error { } _, err = file.Write(source) - return err } @@ -163,7 +140,6 @@ func generateReadMe(models *descriptors.Providers) error { if err = tpl.Execute(buffer, providers); err != nil { return err } - skip = true } @@ -190,29 +166,31 @@ func generateReadMe(models *descriptors.Providers) error { } func orderProviders(models *descriptors.Providers) [][]descriptors.Provider { + providers := append(models.Providers, descriptors.Provider{ + Name: "Manual", + Code: "manual", + }) + const nbCol = 4 - slices.SortFunc(models.Providers, func(a, b descriptors.Provider) int { + slices.SortFunc(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 - ) + var matrix [][]descriptors.Provider + var row []descriptors.Provider - for i, p := range models.Providers { + for i, p := range providers { switch { case len(row) == nbCol: matrix = append(matrix, row) row = []descriptors.Provider{p} - case i == len(models.Providers)-1: + case i == len(providers)-1: row = append(row, p) for j := len(row); j < nbCol; j++ { row = append(row, descriptors.Provider{}) } - matrix = append(matrix, row) default: @@ -224,7 +202,6 @@ func orderProviders(models *descriptors.Providers) [][]descriptors.Provider { for j := len(row); j < nbCol; j++ { row = append(row, descriptors.Provider{}) } - matrix = append(matrix, row) } diff --git a/internal/dns/docs/templates/dns.go.tmpl b/internal/dns/docs/templates/dns.go.tmpl index c1896c91a..e8b336254 100644 --- a/internal/dns/docs/templates/dns.go.tmpl +++ b/internal/dns/docs/templates/dns.go.tmpl @@ -12,6 +12,7 @@ import ( func allDNSCodes() string { providers := []string{ + "manual", {{- range $provider := .Providers }} "{{ $provider.Code }}", {{- end}} @@ -47,6 +48,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/providers/dns_providers.go.tmpl b/internal/dns/providers/dns_providers.go.tmpl index c974ef6a9..2030a3ed0 100644 --- a/internal/dns/providers/dns_providers.go.tmpl +++ b/internal/dns/providers/dns_providers.go.tmpl @@ -6,6 +6,7 @@ import ( "fmt" "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/challenge/dns01" {{- range $provider := .Providers }} "github.com/go-acme/lego/v4/providers/dns/{{ cleanName $provider.Code }}" {{- end}} @@ -14,6 +15,8 @@ import ( // NewDNSChallengeProviderByName Factory for DNS providers. func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { switch name { + case "manual": + return dns01.NewDNSProviderManual() {{- range $provider := .Providers }} case "{{ $provider.Code }}"{{range $alias := $provider.Aliases }},"{{ $alias }}"{{end}}: return {{ cleanName $provider.Code }}.NewDNSProvider() diff --git a/internal/dns/providers/generator.go b/internal/dns/providers/generator.go index df3f8a2e6..bab31072d 100644 --- a/internal/dns/providers/generator.go +++ b/internal/dns/providers/generator.go @@ -46,9 +46,8 @@ func generate() error { defer func() { _ = file.Close() }() b := &bytes.Buffer{} - err = template.Must( - template.New("").Funcs(map[string]any{ + template.New("").Funcs(map[string]interface{}{ "cleanName": func(src string) string { return strings.ReplaceAll(src, "-", "") }, diff --git a/internal/releaser/generator.go b/internal/releaser/generator.go index f24aea25f..d1b3e74e1 100644 --- a/internal/releaser/generator.go +++ b/internal/releaser/generator.go @@ -33,7 +33,7 @@ type Generator struct { targetFile string } -func NewGenerator(templatePath, targetFile string) *Generator { +func NewGenerator(templatePath string, targetFile string) *Generator { return &Generator{templatePath: templatePath, targetFile: targetFile} } diff --git a/internal/releaser/releaser.go b/internal/releaser/releaser.go index 57b463933..6047c427c 100644 --- a/internal/releaser/releaser.go +++ b/internal/releaser/releaser.go @@ -108,7 +108,6 @@ func detach(_ *cli.Context) error { 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 @@ -142,7 +141,6 @@ func (v visitor) Visit(n ast.Node) ast.Visitor { if !ok { continue } - if len(valueSpec.Names) != 1 || len(valueSpec.Values) != 1 { continue } @@ -151,7 +149,6 @@ func (v visitor) Visit(n ast.Node) ast.Visitor { if !ok { continue } - if va.Kind != token.STRING { continue } @@ -167,7 +164,6 @@ func (v visitor) Visit(n ast.Node) ast.Visitor { default: // noop } - return v } diff --git a/lego/client.go b/lego/client.go index d06956203..1109e1224 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, OverallRequestLimit: config.Certificate.OverallRequestLimit}) return &Client{ Certificate: certifier, diff --git a/lego/client_config.go b/lego/client_config.go index 969135a13..fdf1a55f8 100644 --- a/lego/client_config.go +++ b/lego/client_config.go @@ -64,7 +64,6 @@ type CertificateConfig struct { KeyType certcrypto.KeyType Timeout time.Duration OverallRequestLimit int - DisableCommonName bool } // createDefaultHTTPClient Creates an HTTP client with a reasonable timeout value 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..7ba7f08d0 100644 --- a/providers/dns/acmedns/acmedns.go +++ b/providers/dns/acmedns/acmedns.go @@ -3,17 +3,13 @@ package acmedns import ( - "context" "errors" "fmt" - "strings" + "github.com/cpu/goacmedns" "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/acmedns/internal" - "github.com/nrdcg/goacmedns" - "github.com/nrdcg/goacmedns/storage" ) const ( @@ -23,112 +19,56 @@ 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 +105,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 +137,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..f4632411b 100644 --- a/providers/dns/acmedns/acmedns.toml +++ b/providers/dns/acmedns/acmedns.toml @@ -8,23 +8,14 @@ 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" -d '*.example.com' -d example.com 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..68e8f7406 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 +} + +// errorUpdateClient 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..9129eef09 100644 --- a/providers/dns/alidns/alidns.go +++ b/providers/dns/alidns/alidns.go @@ -2,19 +2,18 @@ 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/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" "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" ) @@ -27,7 +26,6 @@ const ( EnvSecretKey = envNamespace + "SECRET_KEY" EnvSecurityToken = envNamespace + "SECURITY_TOKEN" EnvRegionID = envNamespace + "REGION_ID" - EnvLine = envNamespace + "LINE" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" @@ -46,7 +44,6 @@ type Config struct { SecretKey string SecurityToken string RegionID string - Line string PropagationTimeout time.Duration PollingInterval time.Duration TTL int @@ -76,7 +73,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 +102,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 +132,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 +204,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 +257,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..e2d5af8f8 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 -d '*.example.com' -d example.com 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 - -d '*.example.com' -d example.com 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/aliesa/aliesa_test.go b/providers/dns/aliesa/aliesa_test.go deleted file mode 100644 index 025529409..000000000 --- a/providers/dns/aliesa/aliesa_test.go +++ /dev/null @@ -1,151 +0,0 @@ -package aliesa - -import ( - "testing" - - "github.com/go-acme/lego/v4/platform/tester" - "github.com/stretchr/testify/require" -) - -const envDomain = envNamespace + "DOMAIN" - -var envTest = tester.NewEnvTest( - EnvAccessKey, - EnvSecretKey, - EnvRAMRole). - WithDomain(envDomain) - -func TestNewDNSProvider(t *testing.T) { - testCases := []struct { - desc string - envVars map[string]string - expected string - }{ - { - desc: "success", - envVars: map[string]string{ - EnvAccessKey: "123", - EnvSecretKey: "456", - }, - }, - { - desc: "success (RAM role)", - envVars: map[string]string{ - EnvRAMRole: "LegoInstanceRole", - }, - }, - { - desc: "missing credentials", - envVars: map[string]string{ - EnvAccessKey: "", - EnvSecretKey: "", - }, - expected: "aliesa: some credentials information are missing: ALIESA_ACCESS_KEY,ALIESA_SECRET_KEY", - }, - { - desc: "missing access key", - envVars: map[string]string{ - EnvAccessKey: "", - EnvSecretKey: "456", - }, - expected: "aliesa: some credentials information are missing: ALIESA_ACCESS_KEY", - }, - { - desc: "missing secret key", - envVars: map[string]string{ - EnvAccessKey: "123", - EnvSecretKey: "", - }, - expected: "aliesa: some credentials information are missing: ALIESA_SECRET_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 - ramRole string - apiKey string - secretKey string - expected string - }{ - { - desc: "success", - apiKey: "123", - secretKey: "456", - }, - { - desc: "success", - ramRole: "LegoInstanceRole", - }, - { - desc: "missing credentials", - expected: "aliesa: ram role or credentials missing", - }, - { - desc: "missing api key", - secretKey: "456", - expected: "aliesa: ram role or credentials missing", - }, - { - desc: "missing secret key", - apiKey: "123", - expected: "aliesa: ram role or credentials missing", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - config := NewDefaultConfig() - config.APIKey = test.apiKey - config.SecretKey = test.secretKey - config.RAMRole = test.ramRole - - 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) -} diff --git a/providers/dns/allinkl/allinkl.go b/providers/dns/allinkl/allinkl.go index 376b0903c..b1a40ae64 100644 --- a/providers/dns/allinkl/allinkl.go +++ b/providers/dns/allinkl/allinkl.go @@ -11,10 +11,8 @@ import ( "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. @@ -94,16 +92,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 +116,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 +144,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 +162,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 +171,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..4a308d653 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 -d '*.example.com' -d example.com 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..3dd4eee70 100644 --- a/providers/dns/arvancloud/arvancloud.go +++ b/providers/dns/arvancloud/arvancloud.go @@ -13,7 +13,6 @@ 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/arvancloud/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -96,8 +95,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 +164,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..3c0fed4ac 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 -d '*.example.com' -d example.com 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..8a497ffa4 100644 --- a/providers/dns/auroradns/auroradns.go +++ b/providers/dns/auroradns/auroradns.go @@ -10,8 +10,6 @@ import ( "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" ) @@ -53,11 +51,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 +93,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 +161,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..4ee8c0975 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 -d '*.example.com' -d example.com 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..cbd51b830 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.JSONEq(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..61f3005f1 100644 --- a/providers/dns/autodns/autodns.go +++ b/providers/dns/autodns/autodns.go @@ -13,7 +13,6 @@ 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/autodns/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -106,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 } @@ -128,9 +125,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 +144,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..353f223a9 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 -d '*.example.com' -d example.com 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..5702acd8a 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" @@ -38,8 +37,6 @@ const ( EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) -const EnvLegoAzureBypassDeprecation = "LEGO_AZURE_BYPASS_DEPRECATION" - const defaultMetadataEndpoint = "http://169.254.169.254" var _ challenge.ProviderTimeout = (*DNSProvider)(nil) @@ -92,7 +89,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 +96,6 @@ func NewDNSProvider() (*DNSProvider, error) { environmentName := env.GetOrFile(EnvEnvironment) if environmentName != "" { var environment aazure.Environment - switch environmentName { case "china": environment = aazure.ChinaCloud @@ -129,25 +124,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 +148,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if subsID == "" { return nil, errors.New("azure: SubscriptionID is missing") } - config.SubscriptionID = subsID } @@ -179,7 +160,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..d6c9fc7bd 100644 --- a/providers/dns/azure/private.go +++ b/providers/dns/azure/private.go @@ -54,7 +54,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 +81,6 @@ func (d *dnsProviderPrivate) Present(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("azure: %w", err) } - return nil } @@ -108,7 +106,6 @@ func (d *dnsProviderPrivate) CleanUp(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("azure: %w", err) } - return nil } diff --git a/providers/dns/azure/public.go b/providers/dns/azure/public.go index 194956c9c..8e6fa182a 100644 --- a/providers/dns/azure/public.go +++ b/providers/dns/azure/public.go @@ -54,7 +54,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 +81,6 @@ func (d *dnsProviderPublic) Present(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("azure: %w", err) } - return nil } @@ -108,7 +106,6 @@ func (d *dnsProviderPublic) CleanUp(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("azure: %w", err) } - return nil } diff --git a/providers/dns/azuredns/azuredns.go b/providers/dns/azuredns/azuredns.go index b8effadea..860d19691 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,6 +46,9 @@ const ( EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + + EnvGitHubOIDCRequestURL = "ACTIONS_ID_TOKEN_REQUEST_URL" + EnvGitHubOIDCRequestToken = "ACTIONS_ID_TOKEN_REQUEST_TOKEN" ) var _ challenge.ProviderTimeout = (*DNSProvider)(nil) @@ -76,9 +73,6 @@ type Config struct { OIDCRequestURL string OIDCRequestToken string - ServiceConnectionID string - SystemAccessToken string - AuthMethod string AuthMSITimeout time.Duration @@ -140,22 +134,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 +157,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 +193,88 @@ 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 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 +} diff --git a/providers/dns/azuredns/azuredns.toml b/providers/dns/azuredns/azuredns.toml index 7c800ce7e..1f160a856 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 --email you@example.com --dns azuredns -d '*.example.com' -d example.com 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 --email you@example.com --dns azuredns -d '*.example.com' -d example.com run ### Using Azure CLI az login \ -lego --dns azuredns -d '*.example.com' -d example.com run +lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com run ### Using Managed Identity (Azure VM) AZURE_TENANT_ID= \ AZURE_RESOURCE_GROUP= \ -lego --dns azuredns -d '*.example.com' -d example.com run +lego --email you@example.com --dns azuredns -d '*.example.com' -d example.com 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 --email you@example.com --dns azuredns -d '*.example.com' -d example.com 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..24fb3d5ee 100644 --- a/providers/dns/azuredns/private.go +++ b/providers/dns/azuredns/private.go @@ -181,7 +181,6 @@ 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 diff --git a/providers/dns/azuredns/public.go b/providers/dns/azuredns/public.go index 79b6e783f..f7e46150d 100644 --- a/providers/dns/azuredns/public.go +++ b/providers/dns/azuredns/public.go @@ -179,7 +179,6 @@ 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 diff --git a/providers/dns/azuredns/servicediscovery.go b/providers/dns/azuredns/servicediscovery.go index 50a41da37..882e19241 100644 --- a/providers/dns/azuredns/servicediscovery.go +++ b/providers/dns/azuredns/servicediscovery.go @@ -46,7 +46,6 @@ func discoverDNSZones(ctx context.Context, config *Config, credentials azcore.To } zones := map[string]ServiceDiscoveryZone{} - for { // create the query request request := armresourcegraph.QueryRequest{ 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..fbaddcbec 100644 --- a/providers/dns/bindman/bindman.go +++ b/providers/dns/bindman/bindman.go @@ -10,8 +10,7 @@ import ( "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. @@ -49,7 +48,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 +75,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 +92,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 +102,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..4befe9e9d 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 -d '*.example.com' -d example.com 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..8ba026f49 100644 --- a/providers/dns/bluecat/bluecat.go +++ b/providers/dns/bluecat/bluecat.go @@ -13,7 +13,6 @@ import ( "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. @@ -111,8 +110,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/bluecat/bluecat.toml b/providers/dns/bluecat/bluecat.toml index 15df6ed34..e7eb45664 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 -d '*.example.com' -d example.com run ''' [Configuration] @@ -22,10 +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_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" BLUECAT_SKIP_DEPLOY = "Skip deployements" [Links] 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..437d1642a 100644 --- a/providers/dns/brandit/brandit.go +++ b/providers/dns/brandit/brandit.go @@ -13,7 +13,6 @@ 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/brandit/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -93,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, @@ -168,7 +165,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 +183,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..1c70eb1ca 100644 --- a/providers/dns/brandit/brandit.toml +++ b/providers/dns/brandit/brandit.toml @@ -12,7 +12,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 you@example.com --dns brandit -d '*.example.com' -d example.com run ''' [Configuration] @@ -20,10 +20,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..59c57419a 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) @@ -185,7 +183,6 @@ func sign(apiUsername, apiKey string, query url.Values) (url.Values, error) { canonicalRequest := fmt.Sprintf("%s%s%s", apiUsername, timestamp, defaultBaseURL) mac := hmac.New(sha256.New, []byte(apiKey)) - _, 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..9716a20c7 100644 --- a/providers/dns/bunny/bunny.go +++ b/providers/dns/bunny/bunny.go @@ -5,16 +5,14 @@ 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/miekg/dns" "github.com/nrdcg/bunny-go" "golang.org/x/net/publicsuffix" ) @@ -28,7 +26,6 @@ const ( EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) const minTTL = 60 @@ -37,12 +34,10 @@ 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 +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), - HTTPClient: &http.Client{ - Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), - }, + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), } } @@ -91,19 +83,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. @@ -159,12 +141,10 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } var record *bunny.DNSRecord - for _, r := range zone.Records { if ptr.Deref(r.Name) == subDomain && ptr.Deref(r.Type) == bunny.DNSRecordTypeTXT { r := r record = &r - break } } @@ -200,7 +180,6 @@ func findZone(zones *bunny.DNSZones, domain string) *bunny.DNSZone { var domainLength int var zone *bunny.DNSZone - for _, item := range zones.Items { if item == nil { continue @@ -221,14 +200,16 @@ func findZone(zones *bunny.DNSZones, domain string) *bunny.DNSZone { func possibleDomains(domain string) []string { var domains []string - tld, _ := publicsuffix.PublicSuffix(domain) - for d := range dns01.DomainsSeq(domain) { - if tld == d { + labelIndexes := dns.Split(domain) + + for _, index := range labelIndexes { + tld, _ := publicsuffix.PublicSuffix(domain) + if tld == domain[index:] { // skip the TLD break } - domains = append(domains, dns01.UnFqdn(d)) + domains = append(domains, dns01.UnFqdn(domain[index:])) } return domains diff --git a/providers/dns/bunny/bunny.toml b/providers/dns/bunny/bunny.toml index 758c4f202..22b119bbb 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 -d '*.example.com' -d example.com 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..4cf0f6b01 100644 --- a/providers/dns/bunny/bunny_test.go +++ b/providers/dns/bunny/bunny_test.go @@ -40,7 +40,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 +107,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -122,7 +120,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/checkdomain/checkdomain.go b/providers/dns/checkdomain/checkdomain.go index 4bc926ed9..e2d7a05aa 100644 --- a/providers/dns/checkdomain/checkdomain.go +++ b/providers/dns/checkdomain/checkdomain.go @@ -13,7 +13,6 @@ 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/checkdomain/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -73,7 +72,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 +86,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..309b1dfa1 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 -d '*.example.com' -d example.com 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..e2ee41bd4 100644 --- a/providers/dns/civo/civo.go +++ b/providers/dns/civo/civo.go @@ -2,17 +2,14 @@ package civo import ( - "context" "errors" "fmt" - "net/http" "time" + "github.com/civo/civogo" "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/civo/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -24,7 +21,6 @@ const ( EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) const ( @@ -37,12 +33,11 @@ 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 +46,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 +84,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 +96,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 +103,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 +113,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 +130,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 +137,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 +152,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 +160,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 +173,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..fe29364a4 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 -d '*.example.com' -d example.com 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..379dd3cf2 100644 --- a/providers/dns/clouddns/clouddns.go +++ b/providers/dns/clouddns/clouddns.go @@ -12,7 +12,6 @@ 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/clouddns/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -94,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 } diff --git a/providers/dns/clouddns/clouddns.toml b/providers/dns/clouddns/clouddns.toml index 6f516e834..1927e21b5 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 -d '*.example.com' -d example.com 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..ded6150e3 100644 --- a/providers/dns/cloudflare/cloudflare.go +++ b/providers/dns/cloudflare/cloudflare.go @@ -11,25 +11,22 @@ import ( "sync" "time" + "github.com/cloudflare/cloudflare-go" "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/cloudflare/internal" ) // Environment variables names. const ( envNamespace = "CLOUDFLARE_" - EnvEmail = envNamespace + "EMAIL" - EnvAPIKey = envNamespace + "API_KEY" - + 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" @@ -56,8 +53,6 @@ type Config struct { AuthToken string ZoneToken string - BaseURL string - TTL int PropagationTimeout time.Duration PollingInterval time.Duration @@ -69,7 +64,7 @@ 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)), + PollingInterval: env.GetOneWithFallback(EnvPollingInterval, 2*time.Second, env.ParseSecond, altEnvName(EnvPollingInterval)), HTTPClient: &http.Client{ Timeout: env.GetOneWithFallback(EnvHTTPTimeout, 30*time.Second, env.ParseSecond, altEnvName(EnvHTTPTimeout)), }, @@ -104,7 +99,6 @@ func NewDNSProvider() (*DNSProvider, error) { ) if err != nil { var errT error - values, errT = env.GetWithFallback( []string{EnvDNSAPIToken, altEnvName(EnvDNSAPIToken)}, []string{EnvZoneAPIToken, altEnvName(EnvZoneAPIToken), EnvDNSAPIToken, altEnvName(EnvDNSAPIToken)}, @@ -120,7 +114,6 @@ func NewDNSProvider() (*DNSProvider, error) { config.AuthKey = values[EnvAPIKey] config.AuthToken = values[EnvDNSAPIToken] config.ZoneToken = values[EnvZoneAPIToken] - config.BaseURL = env.GetOrFile(EnvBaseURL) return NewDNSProviderConfig(config) } @@ -155,8 +148,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 +155,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 +183,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 +190,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 +199,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 diff --git a/providers/dns/cloudflare/cloudflare.toml b/providers/dns/cloudflare/cloudflare.toml index c46130fe6..0a8295f69 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 -d '*.example.com' -d example.com run # or CLOUDFLARE_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \ -lego --dns cloudflare -d '*.example.com' -d example.com run +lego --email you@example.com --dns cloudflare -d '*.example.com' -d example.com run ''' Additional = ''' @@ -69,11 +69,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 (in seconds)" + CLOUDFLARE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation (in seconds)" + CLOUDFLARE_TTL = "The TTL of the TXT record used for the DNS challenge (in seconds)" + CLOUDFLARE_HTTP_TIMEOUT = "API request timeout (in seconds)" [Links] API = "https://api.cloudflare.com/" diff --git a/providers/dns/cloudflare/cloudflare_test.go b/providers/dns/cloudflare/cloudflare_test.go index 8de9dd848..f026bbc4c 100644 --- a/providers/dns/cloudflare/cloudflare_test.go +++ b/providers/dns/cloudflare/cloudflare_test.go @@ -1,12 +1,10 @@ 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" ) @@ -18,7 +16,6 @@ var envTest = tester.NewEnvTest( EnvAPIKey, EnvDNSAPIToken, EnvZoneAPIToken, - EnvBaseURL, altEnvEmail, altEnvName(EnvAPIKey), altEnvName(EnvDNSAPIToken), @@ -81,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) @@ -178,18 +174,15 @@ func TestNewDNSProviderWithToken(t *testing.T) { } defer envTest.RestoreEnv() - localEnvTest := tester.NewEnvTest( EnvDNSAPIToken, altEnvName(EnvDNSAPIToken), EnvZoneAPIToken, altEnvName(EnvZoneAPIToken), ).WithDomain(envDomain) - 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 +197,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 +231,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 +277,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -294,7 +290,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -303,64 +298,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..ef6524c4d 100644 --- a/providers/dns/cloudns/cloudns.go +++ b/providers/dns/cloudns/cloudns.go @@ -8,14 +8,12 @@ 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. @@ -68,7 +66,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 +99,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 +162,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..dd81da462 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 -d '*.example.com' -d example.com 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..314c20445 100644 --- a/providers/dns/cloudru/cloudru.go +++ b/providers/dns/cloudru/cloudru.go @@ -14,7 +14,6 @@ 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/cloudru/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -61,9 +60,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 +99,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..f795c7ac4 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 -d '*.example.com' -d example.com 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.toml b/providers/dns/cloudxns/cloudxns.toml index 32eae8beb..1486cc4fa 100644 --- a/providers/dns/cloudxns/cloudxns.toml +++ b/providers/dns/cloudxns/cloudxns.toml @@ -9,7 +9,7 @@ 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 -d '*.example.com' -d example.com run ''' [Configuration] @@ -17,7 +17,7 @@ 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" 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..aa6c68ce9 100644 --- a/providers/dns/conoha/conoha.go +++ b/providers/dns/conoha/conoha.go @@ -12,7 +12,6 @@ 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/conoha/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -99,8 +98,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 +120,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..87903365f 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 -d '*.example.com' -d example.com 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..f981b4974 100644 --- a/providers/dns/constellix/constellix.go +++ b/providers/dns/constellix/constellix.go @@ -14,7 +14,6 @@ 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/constellix/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/hashicorp/go-retryablehttp" ) @@ -97,7 +96,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 +199,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..02442d31d 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 -d '*.example.com' -d example.com 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 index cde58a2bf..119b3c16b 100644 --- a/providers/dns/corenetworks/corenetworks.go +++ b/providers/dns/corenetworks/corenetworks.go @@ -11,7 +11,6 @@ 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/corenetworks/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -91,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}, nil } diff --git a/providers/dns/corenetworks/corenetworks.toml b/providers/dns/corenetworks/corenetworks.toml index 09840bb1b..f2bae017c 100644 --- a/providers/dns/corenetworks/corenetworks.toml +++ b/providers/dns/corenetworks/corenetworks.toml @@ -7,7 +7,7 @@ Since = "v4.20.0" Example = ''' CORENETWORKS_LOGIN="xxxx" \ CORENETWORKS_PASSWORD="yyyy" \ -lego --dns corenetworks -d '*.example.com' -d example.com run +lego --email you@example.com --dns corenetworks -d '*.example.com' -d example.com run ''' [Configuration] @@ -15,11 +15,11 @@ lego --dns corenetworks -d '*.example.com' -d example.com run 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)" + CORENETWORKS_POLLING_INTERVAL = "Time between DNS propagation check" + CORENETWORKS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + CORENETWORKS_TTL = "The TTL of the TXT record used for the DNS challenge" + CORENETWORKS_HTTP_TIMEOUT = "API request timeout" + CORENETWORKS_SEQUENCE_INTERVAL = "Time between sequential requests" [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 index 911693468..3cd80f88d 100644 --- a/providers/dns/corenetworks/corenetworks_test.go +++ b/providers/dns/corenetworks/corenetworks_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) @@ -112,7 +111,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -126,7 +124,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/corenetworks/internal/client.go b/providers/dns/corenetworks/internal/client.go index bdc17f2c1..993b01f1e 100644 --- a/providers/dns/corenetworks/internal/client.go +++ b/providers/dns/corenetworks/internal/client.go @@ -38,7 +38,7 @@ func NewClient(login, password string) *Client { // 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) { +func (c Client) ListZone(ctx context.Context) ([]Zone, error) { endpoint := c.baseURL.JoinPath("dnszones") req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) @@ -47,7 +47,6 @@ func (c *Client) ListZone(ctx context.Context) ([]Zone, error) { } var zones []Zone - err = c.do(req, &zones) if err != nil { return nil, err @@ -58,7 +57,7 @@ func (c *Client) ListZone(ctx context.Context) ([]Zone, error) { // 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) { +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) @@ -67,7 +66,6 @@ func (c *Client) GetZoneDetails(ctx context.Context, zone string) (*ZoneDetails, } var details ZoneDetails - err = c.do(req, &details) if err != nil { return nil, err @@ -78,7 +76,7 @@ func (c *Client) GetZoneDetails(ctx context.Context, zone string) (*ZoneDetails, // 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) { +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) @@ -87,7 +85,6 @@ func (c *Client) ListRecords(ctx context.Context, zone string) ([]Record, error) } var records []Record - err = c.do(req, &records) if err != nil { return nil, err @@ -98,7 +95,7 @@ func (c *Client) ListRecords(ctx context.Context, zone string) ([]Record, error) // 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 { +func (c Client) AddRecord(ctx context.Context, zone string, record Record) error { endpoint := c.baseURL.JoinPath("dnszones", zone, "records", "/") if record.Name == "" { @@ -120,7 +117,7 @@ func (c *Client) AddRecord(ctx context.Context, zone string, record Record) erro // 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 { +func (c Client) DeleteRecords(ctx context.Context, zone string, record Record) error { endpoint := c.baseURL.JoinPath("dnszones", zone, "records", "delete") if record.Name == "" { @@ -142,7 +139,7 @@ func (c *Client) DeleteRecords(ctx context.Context, zone string, record Record) // 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 { +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) @@ -158,7 +155,7 @@ func (c *Client) CommitRecords(ctx context.Context, zone string) error { return nil } -func (c *Client) do(req *http.Request, result any) error { +func (c Client) do(req *http.Request, result any) error { at := getToken(req.Context()) if at != "" { req.Header.Set(authorizationHeader, "Bearer "+at) diff --git a/providers/dns/corenetworks/internal/client_test.go b/providers/dns/corenetworks/internal/client_test.go index ca5c81a65..0fff0d5ae 100644 --- a/providers/dns/corenetworks/internal/client_test.go +++ b/providers/dns/corenetworks/internal/client_test.go @@ -1,36 +1,115 @@ 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.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("user", "secret") + client.baseURL, _ = url.Parse(server.URL) + client.HTTPClient = server.Client() + + return client, mux +} + +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(`unsupported method: %s`, req.Method), http.StatusMethodNotAllowed) + 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 + } + } +} + +func testHandlerAuth(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 + } + + 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 + } + } +} + +func TestClient_CreateAuthenticationToken(t *testing.T) { + client, mux := setupTest(t) + + mux.HandleFunc("/auth/token", testHandlerAuth(http.MethodPost, http.StatusOK, "auth.json")) + + ctx := context.Background() + + token, err := client.CreateAuthenticationToken(ctx) + require.NoError(t, err) + + expected := &Token{ + Token: "authsecret", + Expires: 123, + } + assert.Equal(t, expected, token) } func TestClient_ListZone(t *testing.T) { - client := mockBuilder(). - Route("GET /dnszones/", - servermock.ResponseFromFixture("ListZone.json")). - Build(t) + client, mux := setupTest(t) - ctx := t.Context() + mux.HandleFunc("/dnszones/", testHandler(http.MethodGet, http.StatusOK, "ListZone.json")) + + ctx := context.Background() zones, err := client.ListZone(ctx) require.NoError(t, err) @@ -44,12 +123,13 @@ func TestClient_ListZone(t *testing.T) { } func TestClient_GetZoneDetails(t *testing.T) { - client := mockBuilder(). - Route("GET /dnszones/example.com", - servermock.ResponseFromFixture("GetZoneDetails.json")). - Build(t) + client, mux := setupTest(t) - zone, err := client.GetZoneDetails(t.Context(), "example.com") + mux.HandleFunc("/dnszones/example.com", testHandler(http.MethodGet, http.StatusOK, "GetZoneDetails.json")) + + ctx := context.Background() + + zone, err := client.GetZoneDetails(ctx, "example.com") require.NoError(t, err) expected := &ZoneDetails{ @@ -63,12 +143,13 @@ func TestClient_GetZoneDetails(t *testing.T) { } func TestClient_ListRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /dnszones/example.com/records/", - servermock.ResponseFromFixture("ListRecords.json")). - Build(t) + client, mux := setupTest(t) - records, err := client.ListRecords(t.Context(), "example.com") + mux.HandleFunc("/dnszones/example.com/records/", testHandler(http.MethodGet, http.StatusOK, "ListRecords.json")) + + ctx := context.Background() + + records, err := client.ListRecords(ctx, "example.com") require.NoError(t, err) expected := []Record{ @@ -96,35 +177,38 @@ func TestClient_ListRecords(t *testing.T) { } func TestClient_AddRecord(t *testing.T) { - client := mockBuilder(). - Route("POST /dnszones/example.com/records/", - servermock.Noop().WithStatusCode(http.StatusNoContent)). - Build(t) + client, mux := setupTest(t) + + mux.HandleFunc("/dnszones/example.com/records/", testHandler(http.MethodPost, http.StatusNoContent, "")) + + ctx := context.Background() record := Record{Name: "www", TTL: 3600, Type: "A", Data: "127.0.0.1"} - err := client.AddRecord(t.Context(), "example.com", record) + err := client.AddRecord(ctx, "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) + client, mux := setupTest(t) + + mux.HandleFunc("/dnszones/example.com/records/delete", testHandler(http.MethodPost, http.StatusNoContent, "")) + + ctx := context.Background() record := Record{Name: "www", Type: "A", Data: "127.0.0.1"} - err := client.DeleteRecords(t.Context(), "example.com", record) + err := client.DeleteRecords(ctx, "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) + client, mux := setupTest(t) - err := client.CommitRecords(t.Context(), "example.com") + mux.HandleFunc("/dnszones/example.com/records/commit", testHandler(http.MethodPost, http.StatusNoContent, "")) + + ctx := context.Background() + + err := client.CommitRecords(ctx, "example.com") require.NoError(t, err) } diff --git a/providers/dns/corenetworks/internal/identity.go b/providers/dns/corenetworks/internal/identity.go index a7e7448c0..6a3b4d46a 100644 --- a/providers/dns/corenetworks/internal/identity.go +++ b/providers/dns/corenetworks/internal/identity.go @@ -13,7 +13,7 @@ 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) { +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}) @@ -22,7 +22,6 @@ func (c *Client) CreateAuthenticationToken(ctx context.Context) (*Token, error) } var token Token - err = c.do(req, &token) if err != nil { return nil, err @@ -31,7 +30,7 @@ func (c *Client) CreateAuthenticationToken(ctx context.Context) (*Token, error) return &token, nil } -func (c *Client) CreateAuthenticatedContext(ctx context.Context) (context.Context, error) { +func (c Client) CreateAuthenticatedContext(ctx context.Context) (context.Context, error) { tok, err := c.CreateAuthenticationToken(ctx) if err != nil { return nil, err 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/cpanel/cpanel.go b/providers/dns/cpanel/cpanel.go index f335c0a8c..4c80e4db8 100644 --- a/providers/dns/cpanel/cpanel.go +++ b/providers/dns/cpanel/cpanel.go @@ -17,7 +17,6 @@ import ( "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. @@ -147,16 +146,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 +220,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 +235,6 @@ func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { } var newData []string - for _, dataB64 := range existingRecord.DataB64 { if dataB64 == valueB64 { continue @@ -301,7 +291,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 +314,6 @@ func createClient(config *Config) (apiClient, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return client, nil case "whm": @@ -339,8 +326,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..10f75b385 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 -d '*.example.com' -d example.com 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 -d '*.example.com' -d example.com 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..6e726620a 100644 --- a/providers/dns/derak/derak.go +++ b/providers/dns/derak/derak.go @@ -14,7 +14,6 @@ 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/derak/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" "github.com/miekg/dns" ) @@ -95,8 +94,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 +160,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..202d20834 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 you@example.com --dns derak -d '*.example.com' -d example.com 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..9d1e20e53 100644 --- a/providers/dns/desec/desec.go +++ b/providers/dns/desec/desec.go @@ -12,7 +12,6 @@ import ( "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" ) @@ -89,9 +88,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) @@ -180,7 +176,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..6f5486027 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 -d '*.example.com' -d example.com 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..e2a5721c0 100644 --- a/providers/dns/designate/designate.go +++ b/providers/dns/designate/designate.go @@ -68,9 +68,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 +85,7 @@ func NewDNSProvider() (*DNSProvider, error) { opts, erro := clientconfig.AuthOptions(&clientconfig.ClientOpts{ Cloud: val[EnvCloud], }) + if erro != nil { return nil, fmt.Errorf("designate: %w", erro) } @@ -202,7 +202,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 +241,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 +259,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 diff --git a/providers/dns/designate/designate.toml b/providers/dns/designate/designate.toml index a36034f64..aec11eb1e 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 -d '*.example.com' -d example.com 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 -d '*.example.com' -d example.com 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 -d '*.example.com' -d example.com run ''' Additional = ''' @@ -64,9 +64,9 @@ Public cloud providers with support for Designate: 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..976b1f2e6 100644 --- a/providers/dns/digitalocean/digitalocean.go +++ b/providers/dns/digitalocean/digitalocean.go @@ -14,7 +14,6 @@ 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/digitalocean/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -47,7 +46,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 +88,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) @@ -153,7 +147,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..ef2e9de7c 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 -d '*.example.com' -d example.com 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 index 8dfa132ae..de9b14945 100644 --- a/providers/dns/directadmin/directadmin.go +++ b/providers/dns/directadmin/directadmin.go @@ -11,7 +11,6 @@ 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/directadmin/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -100,8 +99,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/directadmin/directadmin.toml b/providers/dns/directadmin/directadmin.toml index 294eaca1c..6b9f1353f 100644 --- a/providers/dns/directadmin/directadmin.toml +++ b/providers/dns/directadmin/directadmin.toml @@ -8,7 +8,7 @@ Example = ''' DIRECTADMIN_API_URL="http://example.com:2222" \ DIRECTADMIN_USERNAME=xxxx \ DIRECTADMIN_PASSWORD=yyy \ -lego --dns directadmin -d '*.example.com' -d example.com run +lego --email you@example.com --dns directadmin -d '*.example.com' -d example.com run ''' [Configuration] @@ -18,10 +18,10 @@ lego --dns directadmin -d '*.example.com' -d example.com run 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)" + DIRECTADMIN_POLLING_INTERVAL = "Time between DNS propagation check" + DIRECTADMIN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + DIRECTADMIN_TTL = "The TTL of the TXT record used for the DNS challenge" + DIRECTADMIN_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.directadmin.com/api.php" diff --git a/providers/dns/directadmin/directadmin_test.go b/providers/dns/directadmin/directadmin_test.go index aed3ba505..10c079f73 100644 --- a/providers/dns/directadmin/directadmin_test.go +++ b/providers/dns/directadmin/directadmin_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 TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -150,7 +148,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/directadmin/internal/client.go b/providers/dns/directadmin/internal/client.go index 64409a79d..fb84257bc 100644 --- a/providers/dns/directadmin/internal/client.go +++ b/providers/dns/directadmin/internal/client.go @@ -38,7 +38,7 @@ func NewClient(baseURL, username, password string) (*Client, error) { }, nil } -func (c *Client) SetRecord(ctx context.Context, domain string, record Record) error { +func (c Client) SetRecord(ctx context.Context, domain string, record Record) error { data, err := querystring.Values(record) if err != nil { return err @@ -49,7 +49,7 @@ func (c *Client) SetRecord(ctx context.Context, domain string, record Record) er return c.do(ctx, domain, data) } -func (c *Client) DeleteRecord(ctx context.Context, domain string, record Record) error { +func (c Client) DeleteRecord(ctx context.Context, domain string, record Record) error { data, err := querystring.Values(record) if err != nil { return err @@ -60,7 +60,7 @@ func (c *Client) DeleteRecord(ctx context.Context, domain string, record Record) return c.do(ctx, domain, data) } -func (c *Client) do(ctx context.Context, domain string, data url.Values) error { +func (c Client) do(ctx context.Context, domain string, data url.Values) error { endpoint := c.baseURL.JoinPath("CMD_API_DNS_CONTROL") query := endpoint.Query() @@ -94,7 +94,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/directadmin/internal/client_test.go b/providers/dns/directadmin/internal/client_test.go index 759a7fb4e..ded4769e3 100644 --- a/providers/dns/directadmin/internal/client_test.go +++ b/providers/dns/directadmin/internal/client_test.go @@ -1,48 +1,89 @@ package internal import ( + "context" + "encoding/json" "fmt" + "io" "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(server.URL, "user", "secret") - 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(server.URL, "user", "secret") + client.HTTPClient = server.Client() + + return client, mux } -func newAPIError(reason string, a ...any) APIError { - return APIError{ +func newJSONErrorf(reason string, a ...any) string { + err := APIError{ Message: "Cannot View Dns Record", Result: fmt.Sprintf(reason, a...), } + + data, _ := json.Marshal(err) + + return string(data) +} + +func testHandler(kv map[string]string) func(rw http.ResponseWriter, req *http.Request) { + return 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 + } + + domain := req.URL.Query().Get("domain") + if domain != "example.com" { + http.Error(rw, newJSONErrorf("invalid domain: %s", domain), http.StatusUnauthorized) + return + } + + data, err := io.ReadAll(req.Body) + if err != nil { + http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + values, err := url.ParseQuery(string(data)) + if err != nil { + http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + for k, v := range kv { + actual := values.Get(k) + if v != actual { + http.Error(rw, newJSONErrorf("invalid %q: %s", k, actual), http.StatusBadRequest) + return + } + } + } } 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) + client, mux := setupTest(t) + + kv := map[string]string{ + "action": "add", + "name": "foo", + "type": "TXT", + "value": "txtTXTtxt", + "ttl": "123", + } + + mux.HandleFunc("/CMD_API_DNS_CONTROL", testHandler(kv)) record := Record{ Name: "foo", @@ -51,16 +92,16 @@ func TestClient_SetRecord(t *testing.T) { TTL: 123, } - err := client.SetRecord(t.Context(), "example.com", record) + err := client.SetRecord(context.Background(), "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) + client, mux := setupTest(t) + + mux.HandleFunc("/CMD_API_DNS_CONTROL", func(rw http.ResponseWriter, req *http.Request) { + http.Error(rw, newJSONErrorf("OOPS"), http.StatusInternalServerError) + }) record := Record{ Name: "foo", @@ -69,23 +110,22 @@ func TestClient_SetRecord_error(t *testing.T) { TTL: 123, } - err := client.SetRecord(t.Context(), "example.com", record) + err := client.SetRecord(context.Background(), "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) + client, mux := setupTest(t) + + kv := map[string]string{ + "action": "delete", + "name": "foo", + "type": "TXT", + "value": "txtTXTtxt", + "ttl": "", + } + + mux.HandleFunc("/CMD_API_DNS_CONTROL", testHandler(kv)) record := Record{ Name: "foo", @@ -93,16 +133,16 @@ func TestClient_DeleteRecord(t *testing.T) { Value: "txtTXTtxt", } - err := client.DeleteRecord(t.Context(), "example.com", record) + err := client.DeleteRecord(context.Background(), "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) + client, mux := setupTest(t) + + mux.HandleFunc("/CMD_API_DNS_CONTROL", func(rw http.ResponseWriter, req *http.Request) { + http.Error(rw, newJSONErrorf("OOPS"), http.StatusInternalServerError) + }) record := Record{ Name: "foo", @@ -110,6 +150,6 @@ func TestClient_DeleteRecord_error(t *testing.T) { Value: "txtTXTtxt", } - err := client.DeleteRecord(t.Context(), "example.com", record) + err := client.DeleteRecord(context.Background(), "example.com", record) require.EqualError(t, err, "[status code 500] Cannot View Dns Record: OOPS") } 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..3aafb4ef8 100644 --- a/providers/dns/dnshomede/dnshomede.toml +++ b/providers/dns/dnshomede/dnshomede.toml @@ -6,17 +6,17 @@ Since = "v4.10.0" Example = ''' DNSHOMEDE_CREDENTIALS=example.org:password \ -lego --dns dnshomede -d '*.example.com' -d example.com run +lego --email you@example.com --dns dnshomede -d '*.example.com' -d example.com 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 -d my.example.org -d 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)" + 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..db80eb80c 100644 --- a/providers/dns/dnsimple/dnsimple.go +++ b/providers/dns/dnsimple/dnsimple.go @@ -8,11 +8,10 @@ import ( "strconv" "time" - "github.com/dnsimple/dnsimple-go/v4/dnsimple" + "github.com/dnsimple/dnsimple-go/dnsimple" "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" "golang.org/x/oauth2" ) @@ -80,14 +79,8 @@ 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}), - ), - ), - ) + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: config.AccessToken}) + client := dnsimple.NewClient(oauth2.NewClient(context.Background(), ts)) client.SetUserAgent(useragent.Get()) if config.BaseURL != "" { @@ -101,16 +94,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 +111,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 +121,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 +150,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 +198,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 +220,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..4d31daae1 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 -d '*.example.com' -d example.com 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..fcfe6714c 100644 --- a/providers/dns/dnsmadeeasy/dnsmadeeasy.go +++ b/providers/dns/dnsmadeeasy/dnsmadeeasy.go @@ -15,7 +15,6 @@ 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/dnsmadeeasy/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -113,12 +112,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 +149,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 +171,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 +178,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..28b38e771 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 -d '*.example.com' -d example.com 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..ab8f20c8d 100644 --- a/providers/dns/dnspod/dnspod.go +++ b/providers/dns/dnspod/dnspod.go @@ -11,7 +11,6 @@ import ( "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" ) @@ -83,12 +82,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 +129,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return err } } - return nil } @@ -157,7 +150,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 +157,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 +185,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..7723f12ed 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 -d '*.example.com' -d example.com 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..9f307f046 100644 --- a/providers/dns/dode/dode.go +++ b/providers/dns/dode/dode.go @@ -12,7 +12,6 @@ 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/dode/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -86,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}, nil } diff --git a/providers/dns/dode/dode.toml b/providers/dns/dode/dode.toml index eb629bb3e..a6a6e8f29 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 -d '*.example.com' -d example.com 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..91fa439c7 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, clearRecord bool) error { endpoint := c.baseURL.JoinPath("letsencrypt") query := endpoint.Query() @@ -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..c194f5608 100644 --- a/providers/dns/domeneshop/domeneshop.go +++ b/providers/dns/domeneshop/domeneshop.go @@ -12,7 +12,6 @@ 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/domeneshop/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -87,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}, nil } diff --git a/providers/dns/domeneshop/domeneshop.toml b/providers/dns/domeneshop/domeneshop.toml index b74af598e..8dfe806e5 100644 --- a/providers/dns/domeneshop/domeneshop.toml +++ b/providers/dns/domeneshop/domeneshop.toml @@ -8,7 +8,7 @@ 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 -d '*.example.com' -d example.com run ''' Additional = ''' @@ -24,9 +24,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..5b4960ee0 100644 --- a/providers/dns/dreamhost/dreamhost.go +++ b/providers/dns/dreamhost/dreamhost.go @@ -14,7 +14,6 @@ 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/dreamhost/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -87,8 +86,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 +96,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..a359ad97f 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 -d '*.example.com' -d example.com 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..687f5bbac 100644 --- a/providers/dns/duckdns/duckdns.go +++ b/providers/dns/duckdns/duckdns.go @@ -13,7 +13,6 @@ 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/duckdns/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -87,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}, nil } diff --git a/providers/dns/duckdns/duckdns.toml b/providers/dns/duckdns/duckdns.toml index 6866da57c..a0ae92c2d 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 -d '*.example.com' -d example.com 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..0ed1bc864 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, clearRecord bool) error { + endpoint, _ := url.Parse(defaultBaseURL) mainDomain := getMainDomain(domain) if mainDomain == "" { @@ -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..627626df6 100644 --- a/providers/dns/dyn/dyn.go +++ b/providers/dns/dyn/dyn.go @@ -12,7 +12,6 @@ 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/dyn/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -93,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 } diff --git a/providers/dns/dyn/dyn.toml b/providers/dns/dyn/dyn.toml index c4b3563e0..e7607d0a2 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 -d '*.example.com' -d example.com 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..af602ddfc 100644 --- a/providers/dns/dynu/dynu.go +++ b/providers/dns/dynu/dynu.go @@ -12,7 +12,6 @@ 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/dynu/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -87,8 +86,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..7d12b428e 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 -d '*.example.com' -d example.com 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..7f33bc2c0 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) @@ -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..7e5e219cb 100644 --- a/providers/dns/easydns/easydns.go +++ b/providers/dns/easydns/easydns.go @@ -16,7 +16,6 @@ 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/easydns/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -78,7 +77,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 +110,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 +186,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..4c775fb5a 100644 --- a/providers/dns/easydns/easydns.toml +++ b/providers/dns/easydns/easydns.toml @@ -7,7 +7,7 @@ Since = "v2.6.0" Example = ''' EASYDNS_TOKEN=xxx \ EASYDNS_KEY=yyy \ -lego --dns easydns -d '*.example.com' -d example.com run +lego --email you@example.com --dns easydns -d '*.example.com' -d example.com 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..d44d2eaf5 100644 --- a/providers/dns/edgedns/edgedns.go +++ b/providers/dns/edgedns/edgedns.go @@ -2,17 +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" + configdns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" @@ -23,22 +20,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 ( @@ -52,8 +44,7 @@ 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 +56,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 +71,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 +98,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 +111,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 +138,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 +146,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 +163,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 +190,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 +210,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 +238,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..c01500112 100644 --- a/providers/dns/edgedns/edgedns.toml +++ b/providers/dns/edgedns/edgedns.toml @@ -12,7 +12,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 -d '*.example.com' -d example.com run ''' Additional = ''' @@ -42,7 +42,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 +53,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..15fa579ed 100644 --- a/providers/dns/efficientip/efficientip.go +++ b/providers/dns/efficientip/efficientip.go @@ -13,7 +13,6 @@ 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/efficientip/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -92,15 +91,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 +113,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..f03a8026f 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 -d '*.example.com' -d example.com 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..58390faa9 100644 --- a/providers/dns/epik/epik.go +++ b/providers/dns/epik/epik.go @@ -13,7 +13,6 @@ 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/epik/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -87,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}, nil } diff --git a/providers/dns/epik/epik.toml b/providers/dns/epik/epik.toml index faf453581..d0f1fda03 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 -d '*.example.com' -d example.com 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/" 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..9a5385453 100644 --- a/providers/dns/epik/internal/client.go +++ b/providers/dns/epik/internal/client.go @@ -37,7 +37,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 +46,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 +56,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 +67,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 +77,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 +89,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,7 +97,7 @@ func (c *Client) RemoveHostRecord(ctx context.Context, domain, recordID string) return &data, nil } -func (c *Client) do(req *http.Request, result any) error { +func (c Client) do(req *http.Request, result any) error { useragent.SetHeader(req.Header) resp, err := c.HTTPClient.Do(req) @@ -131,7 +128,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 +165,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.toml b/providers/dns/exec/exec.toml index 2f9c77c67..b5a68e36a 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 -d '*.example.com' -d example.com 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,7 @@ 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 --d my.example.org run ``` It will then call the program './update-dns.sh' with like this: @@ -59,7 +59,7 @@ 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 -d 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..4038ee4d4 100644 --- a/providers/dns/exoscale/exoscale.go +++ b/providers/dns/exoscale/exoscale.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "net/http" - "strconv" "time" egoscale "github.com/exoscale/egoscale/v3" @@ -14,7 +13,6 @@ import ( "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" ) @@ -90,7 +88,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { 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.ClientOptWithHTTPClient(&http.Client{Timeout: config.HTTPTimeout}), egoscale.ClientOptWithUserAgent(useragent.Get()), ) if err != nil { @@ -106,7 +104,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) zoneName, recordName, err := d.findZoneAndRecordName(info.EffectiveFQDN) @@ -114,11 +111,10 @@ 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) } @@ -146,7 +142,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,16 +149,15 @@ 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(zone.ID, recordName, info.Value) if err != nil { return err } @@ -193,7 +187,9 @@ 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) { +func (d *DNSProvider) findExistingZone(zoneName string) (*egoscale.DNSDomain, error) { + ctx := context.Background() + zones, err := d.client.ListDNSDomains(ctx) if err != nil { return nil, fmt.Errorf("error while retrieving DNS zones: %w", err) @@ -210,15 +206,16 @@ 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) { +func (d *DNSProvider) findExistingRecordID(zoneID egoscale.UUID, recordName string, value string) (egoscale.UUID, error) { + ctx := context.Background() + records, err := d.client.ListDNSDomainRecords(ctx, 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)) { + if record.Name == recordName && record.Type == egoscale.DNSDomainRecordTypeTXT && record.Content == value { return record.ID, nil } } diff --git a/providers/dns/exoscale/exoscale.toml b/providers/dns/exoscale/exoscale.toml index bcc912b07..28a756413 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 -d '*.example.com' -d example.com run ''' [Configuration] @@ -16,10 +16,10 @@ 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_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..7613f2b8d 100644 --- a/providers/dns/freemyip/freemyip.go +++ b/providers/dns/freemyip/freemyip.go @@ -11,7 +11,6 @@ import ( "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" ) @@ -89,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, diff --git a/providers/dns/freemyip/freemyip.toml b/providers/dns/freemyip/freemyip.toml index adbf9e213..a71538ee3 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 -d '*.example.com' -d example.com 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..dd6622172 100644 --- a/providers/dns/gandi/gandi.go +++ b/providers/dns/gandi/gandi.go @@ -13,7 +13,6 @@ 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/gandi/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -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..be5bc00d2 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 -d '*.example.com' -d example.com 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..3c35245de 100644 --- a/providers/dns/gandiv5/gandiv5.go +++ b/providers/dns/gandiv5/gandiv5.go @@ -15,7 +15,6 @@ import ( "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" ) // Environment variables names. @@ -114,7 +113,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if err != nil { return nil, fmt.Errorf("gandiv5: %w", err) } - client.BaseURL = baseURL } @@ -122,8 +120,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 +160,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { authZone: authZone, fieldName: subDomain, } - return nil } @@ -175,7 +170,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 +184,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..ebeef84b8 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 -d '*.example.com' -d example.com 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..ed12a75dc 100644 --- a/providers/dns/gcloud/gcloud.toml +++ b/providers/dns/gcloud/gcloud.toml @@ -5,29 +5,9 @@ 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 +lego --email you@email.com --dns gcloud -d '*.example.com' -d example.com run ''' [Configuration] @@ -39,10 +19,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..99c716b62 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,19 +11,15 @@ 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" ) @@ -32,12 +27,11 @@ import ( 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" @@ -50,34 +44,32 @@ 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. @@ -93,7 +85,6 @@ func NewDNSProvider() (*DNSProvider, error) { // Use default credentials. project := env.GetOrDefaultString(EnvProject, autodetectProjectID(context.Background())) - return NewDNSProviderCredentials(project) } @@ -104,15 +95,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 +122,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 +162,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 +176,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 +191,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 +200,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 +226,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 +251,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 +261,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 +299,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 +348,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 +356,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 +375,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,60 +384,11 @@ 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 } 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..646c5ab1c 100644 --- a/providers/dns/gcore/gcore.go +++ b/providers/dns/gcore/gcore.go @@ -1,16 +1,17 @@ -// 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" ) // Environment variables names. @@ -25,17 +26,28 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) +const ( + defaultPropagationTimeout = 360 * time.Second + defaultPollingInterval = 20 * time.Second +) + 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 +56,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 +79,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..bd514ac78 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 -d '*.example.com' -d example.com 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..4b0d545ed 100644 --- a/providers/dns/glesys/glesys.go +++ b/providers/dns/glesys/glesys.go @@ -13,7 +13,6 @@ 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/glesys/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -100,8 +99,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 +133,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // save data necessary for CleanUp d.activeRecords[info.EffectiveFQDN] = recordID - return nil } @@ -147,7 +143,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..146b24517 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 -d '*.example.com' -d example.com 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..bc0f42339 100644 --- a/providers/dns/godaddy/godaddy.go +++ b/providers/dns/godaddy/godaddy.go @@ -12,7 +12,6 @@ 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/godaddy/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -47,7 +46,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 +95,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{config: config, client: client}, nil } @@ -131,7 +128,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { } var newRecords []internal.DNSRecord - for _, record := range existingRecords { if record.Data != "" { newRecords = append(newRecords, record) @@ -178,7 +174,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } var recordsToKeep []internal.DNSRecord - for _, record := range existingRecords { if record.Data != info.Value && record.Data != "" { recordsToKeep = append(recordsToKeep, record) diff --git a/providers/dns/godaddy/godaddy.toml b/providers/dns/godaddy/godaddy.toml index b906605b3..aa835d087 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 -d '*.example.com' -d example.com 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..1902fc1fd 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{ @@ -48,7 +48,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 @@ -142,7 +141,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/godaddy/internal/client_test.go b/providers/dns/godaddy/internal/client_test.go index 694a16565..50d193bdb 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", "") + 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.EqualError(t, err, "[status code: 422] INVALID_BODY: Request body doesn't fulfill schema, see details in `fields`") 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,59 @@ 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") + err := client.UpdateTxtRecords(context.Background(), records, "example.com", "lego") require.EqualError(t, err, "[status code: 422] INVALID_BODY: Request body doesn't fulfill schema, see details in `fields`") } func TestClient_DeleteTxtRecords(t *testing.T) { - client := mockBuilder(). - Route("DELETE /v1/domains/example.com/records/TXT/foo", - servermock.Noop().WithStatusCode(http.StatusNoContent)). - Build(t) + client, mux := setupTest(t) - err := client.DeleteTxtRecords(t.Context(), "example.com", "foo") + mux.HandleFunc("/v1/domains/example.com/records/TXT/foo", testHandler(http.MethodDelete, http.StatusNoContent, "")) + + err := client.DeleteTxtRecords(context.Background(), "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) + client, mux := setupTest(t) - err := client.DeleteTxtRecords(t.Context(), "example.com", "foo") + mux.HandleFunc("/v1/domains/example.com/records/TXT/foo", testHandler(http.MethodDelete, http.StatusConflict, "error-extended.json")) + + err := client.DeleteTxtRecords(context.Background(), "example.com", "foo") require.EqualError(t, err, "[status code: 409] ACCESS_DENIED: Authenticated user is not allowed access [test: content (path=/foo) (pathRelated=/bar)]") } + +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.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/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..a97a97896 100644 --- a/providers/dns/godaddy/internal/types.go +++ b/providers/dns/godaddy/internal/types.go @@ -1,9 +1,6 @@ package internal -import ( - "fmt" - "strings" -) +import "fmt" // DNSRecord a DNS record. type DNSRecord struct { @@ -26,16 +23,13 @@ type APIError struct { } func (a APIError) Error() string { - msg := new(strings.Builder) - - _, _ = fmt.Fprintf(msg, "%s: %s", a.Code, a.Message) + msg := fmt.Sprintf("%s: %s", a.Code, a.Message) for _, field := range a.Fields { - msg.WriteString(" ") - msg.WriteString(field.String()) + msg += " " + field.String() } - return msg.String() + return msg } type Field struct { diff --git a/providers/dns/googledomains/googledomains.go b/providers/dns/googledomains/googledomains.go index b5eed0b03..933929147 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. @@ -32,29 +37,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..97e5452cc 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 -d '*.example.com' -d example.com 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..e5c5ca266 100644 --- a/providers/dns/hetzner/hetzner.go +++ b/providers/dns/hetzner/hetzner.go @@ -2,28 +2,28 @@ 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" + "github.com/go-acme/lego/v4/providers/dns/hetzner/internal" ) // Environment variables names. const ( - // Deprecated: use EnvAPIToken instead. - EnvAPIKey = legacy.EnvAPIKey - EnvAPIToken = hetznerv1.EnvAPIToken + envNamespace = "HETZNER_" - EnvTTL = hetznerv1.EnvTTL - EnvPropagationTimeout = hetznerv1.EnvPropagationTimeout - EnvPollingInterval = hetznerv1.EnvPollingInterval - EnvHTTPTimeout = hetznerv1.EnvHTTPTimeout + EnvAPIKey = envNamespace + "API_KEY" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) const minTTL = 60 @@ -32,11 +32,7 @@ var _ challenge.ProviderTimeout = (*DNSProvider)(nil) // 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 +44,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 +53,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 +77,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..77d23acb8 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 -d '*.example.com' -d example.com 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..67c4661bd 100644 --- a/providers/dns/hostingde/hostingde.go +++ b/providers/dns/hostingde/hostingde.go @@ -2,9 +2,11 @@ package hostingde import ( + "context" "errors" "fmt" "net/http" + "sync" "time" "github.com/go-acme/lego/v4/challenge" @@ -29,7 +31,14 @@ const ( 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 { @@ -37,7 +46,7 @@ func NewDefaultConfig() *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 +55,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. @@ -70,36 +83,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 for %s: %w", fqdn, 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..39e7ab0f9 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 -d '*.example.com' -d example.com 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..22d3be7bd 100644 --- a/providers/dns/hosttech/hosttech.go +++ b/providers/dns/hosttech/hosttech.go @@ -14,7 +14,6 @@ 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/hosttech/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -85,11 +84,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 +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("hosttech: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) } @@ -174,9 +168,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..89d495b0c 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 -d '*.example.com' -d example.com 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..41f4ffbf8 100644 --- a/providers/dns/httpnet/httpnet.go +++ b/providers/dns/httpnet/httpnet.go @@ -2,9 +2,12 @@ package httpnet import ( + "context" "errors" "fmt" "net/http" + "net/url" + "sync" "time" "github.com/go-acme/lego/v4/challenge" @@ -26,12 +29,17 @@ 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 { @@ -39,7 +47,7 @@ func NewDefaultConfig() *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 +56,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. @@ -72,36 +84,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 for %s: %w", fqdn, 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..baf170973 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 -d '*.example.com' -d example.com 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..8f8311e0a 100644 --- a/providers/dns/httpreq/httpreq.go +++ b/providers/dns/httpreq/httpreq.go @@ -14,7 +14,6 @@ import ( "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" ) @@ -89,7 +88,6 @@ func NewDNSProvider() (*DNSProvider, error) { config.Username = env.GetOrFile(EnvUsername) config.Password = env.GetOrFile(EnvPassword) config.Endpoint = endpoint - return NewDNSProviderConfig(config) } @@ -103,8 +101,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 +125,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("httpreq: %w", err) } - return nil } @@ -143,7 +138,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("httpreq: %w", err) } - return nil } @@ -162,7 +156,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("httpreq: %w", err) } - return nil } @@ -176,13 +169,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..43f3e4f62 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 -d '*.example.com' -d example.com 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 index e47f3e2b5..fbb594f58 100644 --- a/providers/dns/huaweicloud/huaweicloud.go +++ b/providers/dns/huaweicloud/huaweicloud.go @@ -2,7 +2,6 @@ package huaweicloud import ( - "context" "errors" "fmt" "strconv" @@ -10,12 +9,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/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" @@ -65,7 +62,7 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config - client *internal.DnsClient + client *hwdns.DnsClient recordIDs map[string]string recordIDsMu sync.Mutex @@ -122,7 +119,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return &DNSProvider{ config: config, - client: internal.NewDnsClient(client), + client: hwdns.NewDnsClient(client), recordIDs: map[string]string{}, }, nil } @@ -150,27 +147,19 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { 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) - } + err = wait.For("record set sync on "+domain, d.config.PropagationTimeout, d.config.PollingInterval, func() (bool, error) { + rs, errShow := d.client.ShowRecordSet(&hwmodel.ShowRecordSetRequest{ + ZoneId: zoneID, + RecordsetId: recordSetID, + }) + if errShow != nil { + return false, 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), - ) + return !strings.HasSuffix(ptr.Deref(rs.Status), "PENDING_"), nil + }) if err != nil { - return fmt.Errorf("huaweicloud: record set sync on %s: %w", domain, err) + return fmt.Errorf("huaweicloud: %w", err) } return nil @@ -184,7 +173,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("huaweicloud: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) } @@ -209,10 +197,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("huaweicloud: delete record: %w", err) } - d.recordIDsMu.Lock() - delete(d.recordIDs, token) - d.recordIDsMu.Unlock() - return nil } diff --git a/providers/dns/huaweicloud/huaweicloud.toml b/providers/dns/huaweicloud/huaweicloud.toml index e8d417c11..423dd9d7d 100644 --- a/providers/dns/huaweicloud/huaweicloud.toml +++ b/providers/dns/huaweicloud/huaweicloud.toml @@ -8,7 +8,7 @@ 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 +lego --email you@example.com --dns huaweicloud -d '*.example.com' -d example.com run ''' [Configuration] @@ -18,10 +18,10 @@ lego --dns huaweicloud -d '*.example.com' -d example.com run 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)" + HUAWEICLOUD_POLLING_INTERVAL = "Time between DNS propagation check" + HUAWEICLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + HUAWEICLOUD_TTL = "The TTL of the TXT record used for the DNS challenge" + HUAWEICLOUD_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://console-intl.huaweicloud.com/apiexplorer/#/openapi/DNS/doc?locale=en-us" diff --git a/providers/dns/huaweicloud/huaweicloud_test.go b/providers/dns/huaweicloud/huaweicloud_test.go index 25e295da7..6787650ca 100644 --- a/providers/dns/huaweicloud/huaweicloud_test.go +++ b/providers/dns/huaweicloud/huaweicloud_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) @@ -141,7 +140,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -155,7 +153,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() 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..e2054d38d 100644 --- a/providers/dns/hurricane/hurricane.go +++ b/providers/dns/hurricane/hurricane.go @@ -5,13 +5,13 @@ 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. @@ -58,15 +58,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 +84,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 +122,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..88e73dea9 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 -d '*.example.com' -d example.com 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 -d my.example.org -d demo.example.org ''' Additional = """ @@ -39,10 +39,10 @@ HURRICANE_TOKENS=example.org:token [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)" + 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..890f9f627 100644 --- a/providers/dns/hyperone/hyperone.go +++ b/providers/dns/hyperone/hyperone.go @@ -13,7 +13,6 @@ 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/hyperone/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Environment variables names. @@ -77,7 +76,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 +96,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 +163,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..bebde3185 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 -d '*.example.com' -d example.com 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.toml b/providers/dns/ibmcloud/ibmcloud.toml index 01088f09b..270995465 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 -d '*.example.com' -d example.com 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..9beb411ed 100644 --- a/providers/dns/iij/iij.go +++ b/providers/dns/iij/iij.go @@ -6,6 +6,7 @@ import ( "fmt" "slices" "strconv" + "strings" "time" "github.com/go-acme/lego/v4/challenge" @@ -13,7 +14,6 @@ import ( "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. @@ -98,7 +98,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("iij: %w", err) } - return nil } @@ -111,7 +110,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("iij: %w", err) } - return nil } @@ -228,20 +226,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..da7590dd9 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 -d '*.example.com' -d example.com 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.toml b/providers/dns/iijdpf/iijdpf.toml index 650285f95..297866e2b 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 -d '*.example.com' -d example.com 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..6aefd0bc1 100644 --- a/providers/dns/infoblox/infoblox.go +++ b/providers/dns/infoblox/infoblox.go @@ -12,21 +12,20 @@ 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/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" @@ -58,9 +57,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 +66,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 +83,6 @@ type DNSProvider struct { config *Config transportConfig infoblox.TransportConfig ibConfig infoblox.HostConfig - ibAuth infoblox.AuthConfig recordRefs map[string]string recordRefsMu sync.Mutex @@ -128,22 +122,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,7 +145,7 @@ 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) } @@ -169,7 +154,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { objectManager := infoblox.NewObjectManager(connector, useragent.Get(), "") - 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,7 +170,7 @@ 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) } @@ -198,7 +183,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { 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..ad7cb5cef 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 -d '*.example.com' -d example.com 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..79c6f577e 100644 --- a/providers/dns/infomaniak/infomaniak.go +++ b/providers/dns/infomaniak/infomaniak.go @@ -13,7 +13,6 @@ 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/infomaniak/internal" - "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug" ) // Infomaniak API reference: https://api.infomaniak.com/doc @@ -97,11 +96,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..2de205b8f 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 -d '*.example.com' -d example.com 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/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 index 090c9109a..30b7f6929 100644 --- a/providers/dns/internal/useragent/useragent.go +++ b/providers/dns/internal/useragent/useragent.go @@ -10,12 +10,12 @@ import ( const ( // ourUserAgent is the User-Agent of this underlying library package. - ourUserAgent = "goacme-lego/4.32.0" + ourUserAgent = "goacme-lego/4.21.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" ) // Get builds and returns the User-Agent string. 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/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..9d6c17676 100644 --- a/providers/dns/internetbs/internetbs.go +++ b/providers/dns/internetbs/internetbs.go @@ -11,7 +11,6 @@ import ( "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" ) @@ -89,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, diff --git a/providers/dns/internetbs/internetbs.toml b/providers/dns/internetbs/internetbs.toml index f22850253..054a1f6e9 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 -d '*.example.com' -d example.com 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..f316fd548 100644 --- a/providers/dns/inwx/inwx.go +++ b/providers/dns/inwx/inwx.go @@ -46,7 +46,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), } @@ -177,19 +177,17 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("inwx: %w", err) } - var recordID string - + var recordID int for _, record := range response.Records { if record.Content != info.Value { continue } recordID = record.ID - break } - if recordID == "" { + if recordID == 0 { return errors.New("inwx: TXT record not found") } diff --git a/providers/dns/inwx/inwx.toml b/providers/dns/inwx/inwx.toml index da4c6d959..1186dcf20 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 -d '*.example.com' -d example.com 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 -d '*.example.com' -d example.com 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..d12fd7f09 100644 --- a/providers/dns/ionos/ionos.go +++ b/providers/dns/ionos/ionos.go @@ -2,15 +2,18 @@ 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" ) // Environment variables names. @@ -30,13 +33,19 @@ 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 +55,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 +79,126 @@ 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) + } + + name := dns01.UnFqdn(info.EffectiveFQDN) + + zone := findZone(zones, name) + if zone == nil { + return errors.New("ionos: no matching zone found for domain") + } + + filter := &internal.RecordsFilter{ + Suffix: name, + 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: name, + 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 + name := dns01.UnFqdn(info.EffectiveFQDN) + + zone := findZone(zones, name) + if zone == nil { + return errors.New("ionos: no matching zone found for domain") + } + + filter := &internal.RecordsFilter{ + Suffix: name, + 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 == name && 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..e9bfd7319 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 -d '*.example.com' -d example.com 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..6e8d1c5bb 100644 --- a/providers/dns/ipv64/ipv64.go +++ b/providers/dns/ipv64/ipv64.go @@ -12,7 +12,6 @@ import ( "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" ) @@ -86,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}, nil } diff --git a/providers/dns/ipv64/ipv64.toml b/providers/dns/ipv64/ipv64.toml index aa1720c9e..ece506c34 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 -d '*.example.com' -d example.com 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..2b53377ed 100644 --- a/providers/dns/iwantmyname/iwantmyname.go +++ b/providers/dns/iwantmyname/iwantmyname.go @@ -2,13 +2,16 @@ 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. @@ -38,12 +41,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 +74,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 +102,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..678977029 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 -d '*.example.com' -d example.com 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..1f5acf17f 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 -d '*.example.com' -d example.com 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 -d '*.example.com' -d example.com 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 -d '*.example.com' -d example.com 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..5c623467a 100644 --- a/providers/dns/joker/provider_dmapi.go +++ b/providers/dns/joker/provider_dmapi.go @@ -10,7 +10,6 @@ import ( "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" ) @@ -28,7 +27,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 +66,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 +158,6 @@ func (d *dmapiProvider) CleanUp(domain, token, keyAuth string) error { if err != nil { return formatResponseError(response, err) } - return nil } @@ -171,6 +166,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..991772fe7 100644 --- a/providers/dns/joker/provider_svc.go +++ b/providers/dns/joker/provider_svc.go @@ -9,7 +9,6 @@ import ( "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" ) @@ -48,8 +47,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..a0437b0eb 100644 --- a/providers/dns/liara/liara.go +++ b/providers/dns/liara/liara.go @@ -13,7 +13,6 @@ import ( "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" ) @@ -23,7 +22,6 @@ const ( envNamespace = "LIARA_" EnvAPIKey = envNamespace + "API_KEY" - EnvTeamID = envNamespace + "TEAM_ID" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" @@ -40,9 +38,7 @@ var _ challenge.ProviderTimeout = (*DNSProvider)(nil) // 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 +76,6 @@ func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] - config.TeamID = env.GetOrFile(EnvTeamID) return NewDNSProviderConfig(config) } @@ -104,20 +99,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 +140,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 +165,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..aaa4061f5 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 you@example.com --dns liara -d '*.example.com' -d example.com 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..d07b5505a 100644 --- a/providers/dns/lightsail/lightsail.go +++ b/providers/dns/lightsail/lightsail.go @@ -96,10 +96,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..1b96e87f0 100644 --- a/providers/dns/lightsail/lightsail_integration_test.go +++ b/providers/dns/lightsail/lightsail_integration_test.go @@ -1,6 +1,7 @@ package lightsail import ( + "context" "testing" "github.com/aws/aws-sdk-go-v2/aws" @@ -28,13 +29,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() { diff --git a/providers/dns/lightsail/lightsail_test.go b/providers/dns/lightsail/lightsail_test.go index a6b46045e..14370ffd9 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,29 @@ var envTest = tester.NewEnvTest( WithDomain(EnvDNSZone). WithLiveTestRequirements(envAwsAccessKeyID, envAwsSecretAccessKey, EnvDNSZone) +func makeProvider(serverURL string) *DNSProvider { + config := aws.Config{ + Credentials: credentials.NewStaticCredentialsProvider("abc", "123", " "), + Region: "mock-region", + BaseEndpoint: aws.String(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 +69,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 index ae6ab87eb..8a8b93adb 100644 --- a/providers/dns/limacity/internal/client.go +++ b/providers/dns/limacity/internal/client.go @@ -32,7 +32,7 @@ func NewClient(apiKey string) *Client { } } -func (c *Client) GetDomains(ctx context.Context) ([]Domain, error) { +func (c Client) GetDomains(ctx context.Context) ([]Domain, error) { endpoint := c.baseURL.JoinPath("domains.json") req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) @@ -41,7 +41,6 @@ func (c *Client) GetDomains(ctx context.Context) ([]Domain, error) { } var results DomainsResponse - err = c.do(req, &results) if err != nil { return nil, err @@ -50,7 +49,7 @@ func (c *Client) GetDomains(ctx context.Context) ([]Domain, error) { return results.Data, nil } -func (c *Client) GetRecords(ctx context.Context, domainID int) ([]Record, error) { +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) @@ -59,7 +58,6 @@ func (c *Client) GetRecords(ctx context.Context, domainID int) ([]Record, error) } var results RecordsResponse - err = c.do(req, &results) if err != nil { return nil, err @@ -68,7 +66,7 @@ func (c *Client) GetRecords(ctx context.Context, domainID int) ([]Record, error) return results.Data, nil } -func (c *Client) AddRecord(ctx context.Context, domainID int, record Record) error { +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}) @@ -77,7 +75,6 @@ func (c *Client) AddRecord(ctx context.Context, domainID int, record Record) err } var results APIResponse - err = c.do(req, &results) if err != nil { return err @@ -86,7 +83,7 @@ func (c *Client) AddRecord(ctx context.Context, domainID int, record Record) err return nil } -func (c *Client) UpdateRecord(ctx context.Context, domainID, recordID int, record Record) error { +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}) @@ -95,7 +92,6 @@ func (c *Client) UpdateRecord(ctx context.Context, domainID, recordID int, recor } var results APIResponse - err = c.do(req, &results) if err != nil { return err @@ -104,7 +100,7 @@ func (c *Client) UpdateRecord(ctx context.Context, domainID, recordID int, recor return nil } -func (c *Client) DeleteRecord(ctx context.Context, domainID, recordID int) error { +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)) @@ -114,7 +110,6 @@ func (c *Client) DeleteRecord(ctx context.Context, domainID, recordID int) error } var results APIResponse - err = c.do(req, &results) if err != nil { return err @@ -123,7 +118,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.SetBasicAuth("api", c.apiKey) resp, err := c.HTTPClient.Do(req) @@ -182,7 +177,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/limacity/internal/client_test.go b/providers/dns/limacity/internal/client_test.go index c43f12ba2..b9a13bdab 100644 --- a/providers/dns/limacity/internal/client_test.go +++ b/providers/dns/limacity/internal/client_test.go @@ -1,38 +1,72 @@ 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 = "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() +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - WithBasicAuth("api", apiKey), - ) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient(apiKey) + client.baseURL, _ = url.Parse(server.URL) + + return client, mux +} + +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 + } + + username, key, ok := req.BasicAuth() + if username != "api" || key != apiKey || !ok { + http.Error(rw, http.StatusText(http.StatusUnauthorized), 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 := mockBuilder(). - Route("GET /domains.json", servermock.ResponseFromFixture("get-domains.json")). - Build(t) + client, mux := setupTest(t) - domains, err := client.GetDomains(t.Context()) + mux.HandleFunc("/domains.json", testHandler("get-domains.json", http.MethodGet, http.StatusOK)) + + domains, err := client.GetDomains(context.Background()) require.NoError(t, err) expected := []Domain{{ @@ -46,22 +80,20 @@ func TestClient_GetDomains(t *testing.T) { } func TestClient_GetDomains_error(t *testing.T) { - client := mockBuilder(). - Route("GET /domains.json", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) + client, mux := setupTest(t) - _, err := client.GetDomains(t.Context()) + mux.HandleFunc("/domains.json", testHandler("error.json", http.MethodGet, http.StatusBadRequest)) + + _, err := client.GetDomains(context.Background()) 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) + client, mux := setupTest(t) - records, err := client.GetRecords(t.Context(), 123) + mux.HandleFunc("/domains/123/records.json", testHandler("get-records.json", http.MethodGet, http.StatusOK)) + + records, err := client.GetRecords(context.Background(), 123) require.NoError(t, err) expected := []Record{ @@ -84,22 +116,18 @@ func TestClient_GetRecords(t *testing.T) { } func TestClient_GetRecords_error(t *testing.T) { - client := mockBuilder(). - Route("GET /domains/123/records.json", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) + client, mux := setupTest(t) - _, err := client.GetRecords(t.Context(), 123) + mux.HandleFunc("/domains/123/records.json", testHandler("error.json", http.MethodGet, http.StatusBadRequest)) + + _, err := client.GetRecords(context.Background(), 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) + client, mux := setupTest(t) + + mux.HandleFunc("/domains/123/records.json", testHandler("ok.json", http.MethodPost, http.StatusOK)) record := Record{ Name: "foo", @@ -108,16 +136,14 @@ func TestClient_AddRecord(t *testing.T) { Type: "TXT", } - err := client.AddRecord(t.Context(), 123, record) + err := client.AddRecord(context.Background(), 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) + client, mux := setupTest(t) + + mux.HandleFunc("/domains/123/records.json", testHandler("error.json", http.MethodPost, http.StatusBadRequest)) record := Record{ Name: "foo", @@ -126,49 +152,42 @@ func TestClient_AddRecord_error(t *testing.T) { Type: "TXT", } - err := client.AddRecord(t.Context(), 123, record) + err := client.AddRecord(context.Background(), 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) + client, mux := setupTest(t) - err := client.UpdateRecord(t.Context(), 123, 456, Record{}) + mux.HandleFunc("/domains/123/records/456", testHandler("ok.json", http.MethodPut, http.StatusOK)) + + err := client.UpdateRecord(context.Background(), 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) + client, mux := setupTest(t) - err := client.UpdateRecord(t.Context(), 123, 456, Record{}) + mux.HandleFunc("/domains/123/records/456", testHandler("error.json", http.MethodPut, http.StatusBadRequest)) + + err := client.UpdateRecord(context.Background(), 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) + client, mux := setupTest(t) - err := client.DeleteRecord(t.Context(), 123, 456) + mux.HandleFunc("/domains/123/records/456", testHandler("ok.json", http.MethodDelete, http.StatusOK)) + + err := client.DeleteRecord(context.Background(), 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) + client, mux := setupTest(t) - err := client.DeleteRecord(t.Context(), 123, 456) + mux.HandleFunc("/domains/123/records/456", testHandler("error.json", http.MethodDelete, http.StatusBadRequest)) + + err := client.DeleteRecord(context.Background(), 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/types.go b/providers/dns/limacity/internal/types.go index 7411632ea..5fdbacef9 100644 --- a/providers/dns/limacity/internal/types.go +++ b/providers/dns/limacity/internal/types.go @@ -10,7 +10,7 @@ type RecordsResponse struct { } type NameserverRecordPayload struct { - Data Record `json:"nameserver_record"` + Data Record `json:"nameserver_record,omitempty"` } type DomainsResponse struct { diff --git a/providers/dns/limacity/limacity.go b/providers/dns/limacity/limacity.go index 3291faf66..ef2c6950d 100644 --- a/providers/dns/limacity/limacity.go +++ b/providers/dns/limacity/limacity.go @@ -13,8 +13,8 @@ import ( "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" + "github.com/miekg/dns" ) // Environment variables names. @@ -90,12 +90,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { 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, @@ -117,11 +111,9 @@ func (d *DNSProvider) Sequential() 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) - domains, err := d.client.GetDomains(ctx) + domains, err := d.client.GetDomains(context.Background()) if err != nil { return fmt.Errorf("limacity: get domains: %w", err) } @@ -143,7 +135,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { Type: "TXT", } - err = d.client.AddRecord(ctx, dom.ID, record) + err = d.client.AddRecord(context.Background(), dom.ID, record) if err != nil { return fmt.Errorf("limacity: add record: %w", err) } @@ -157,26 +149,22 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // 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) + records, err := d.client.GetRecords(context.Background(), 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 @@ -188,20 +176,19 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return errors.New("limacity: TXT record not found") } - err = d.client.DeleteRecord(ctx, domainID, recordID) + err = d.client.DeleteRecord(context.Background(), 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) { + labelIndexes := dns.Split(fqdn) + + for _, index := range labelIndexes { + f := fqdn[index:] domain := dns01.UnFqdn(f) for _, dom := range domains { diff --git a/providers/dns/limacity/limacity.toml b/providers/dns/limacity/limacity.toml index d236577d0..c9bcaf16e 100644 --- a/providers/dns/limacity/limacity.toml +++ b/providers/dns/limacity/limacity.toml @@ -6,18 +6,18 @@ Since = "v4.18.0" Example = ''' LIMACITY_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns limacity -d '*.example.com' -d example.com run +lego --email you@example.com --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)" + LIMACITY_POLLING_INTERVAL = "Time between DNS propagation check" + LIMACITY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + LIMACITY_SEQUENCE_INTERVAL = "Time between sequential requests" + LIMACITY_TTL = "The TTL of the TXT record used for the DNS challenge" + LIMACITY_HTTP_TIMEOUT = "API request timeout" [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 index 3301fcb2e..2834a5f1f 100644 --- a/providers/dns/limacity/limacity_test.go +++ b/providers/dns/limacity/limacity_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/linode/linode.go b/providers/dns/linode/linode.go index b03dee4f5..841e24c69 100644 --- a/providers/dns/linode/linode.go +++ b/providers/dns/linode/linode.go @@ -12,7 +12,6 @@ import ( "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" @@ -51,9 +50,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,7 +102,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { }, } - client := linodego.NewClient(clientdebug.Wrap(oauth2Client)) + client := linodego.NewClient(oauth2Client) client.SetUserAgent(useragent.Get()) return &DNSProvider{config: config, client: &client}, nil @@ -131,11 +130,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 +144,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 +168,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 +177,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 +191,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..790a2238c 100644 --- a/providers/dns/linode/linode.toml +++ b/providers/dns/linode/linode.toml @@ -7,17 +7,17 @@ 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 -d '*.example.com' -d example.com 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..76f965123 100644 --- a/providers/dns/liquidweb/liquidweb.go +++ b/providers/dns/liquidweb/liquidweb.go @@ -55,16 +55,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 +159,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 +179,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..987b8027d 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 -d '*.example.com' -d example.com 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..34d4374fb 100644 --- a/providers/dns/loopia/loopia.go +++ b/providers/dns/loopia/loopia.go @@ -12,7 +12,6 @@ import ( "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" ) @@ -35,9 +34,9 @@ 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 +56,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 +113,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..f1065b35e 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 you@example.com --dns loopia -d '*.example.com' -d example.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..ef0a9b7d6 100644 --- a/providers/dns/luadns/luadns.go +++ b/providers/dns/luadns/luadns.go @@ -13,7 +13,6 @@ import ( "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" ) @@ -49,7 +48,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 +100,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..b55751f55 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 -d '*.example.com' -d example.com 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..3ea8a9f29 100644 --- a/providers/dns/mailinabox/mailinabox.go +++ b/providers/dns/mailinabox/mailinabox.go @@ -5,13 +5,11 @@ 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,7 +23,6 @@ const ( EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) var _ challenge.ProviderTimeout = (*DNSProvider)(nil) @@ -37,7 +34,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 +41,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 +81,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..8ee282396 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 -d '*.example.com' -d example.com 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 index b5a7dbae7..89c426b02 100644 --- a/providers/dns/manageengine/internal/client.go +++ b/providers/dns/manageengine/internal/client.go @@ -24,12 +24,12 @@ type Client struct { } // NewClient creates a new Client. -func NewClient(hc *http.Client) *Client { +func NewClient(ctx context.Context, clientID, clientSecret string) *Client { baseURL, _ := url.Parse(defaultBaseURL) return &Client{ baseURL: baseURL, - httpClient: hc, + httpClient: createOAuthClient(ctx, clientID, clientSecret), } } @@ -75,7 +75,7 @@ func (c *Client) GetAllZoneRecords(ctx context.Context, zoneID int) ([]ZoneRecor // 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 { +func (c *Client) DeleteZoneRecord(ctx context.Context, zoneID int, 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) @@ -109,7 +109,6 @@ 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") } @@ -189,7 +188,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/manageengine/internal/client_test.go b/providers/dns/manageengine/internal/client_test.go index 25d1730f6..edf046222 100644 --- a/providers/dns/manageengine/internal/client_test.go +++ b/providers/dns/manageengine/internal/client_test.go @@ -1,35 +1,60 @@ package internal import ( + "context" + "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(server.Client()) +func setupTest(t *testing.T, pattern string, status int, filename string) *Client { + t.Helper() - client.baseURL, _ = url.Parse(server.URL) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) - return client, nil - }, - servermock.CheckHeader(). - WithAccept("application/json")) + mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { + if filename == "" { + rw.WriteHeader(status) + 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(status) + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + client := NewClient(context.Background(), "abc", "secret") + + client.httpClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client } func TestClient_GetAllZones(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/domain", servermock.ResponseFromFixture("zone_domains_all.json")). - Build(t) + client := setupTest(t, "GET /dns/domain", http.StatusOK, "zone_domains_all.json") - groups, err := client.GetAllZones(t.Context()) + groups, err := client.GetAllZones(context.Background()) require.NoError(t, err) expected := []Zone{ @@ -108,24 +133,18 @@ func TestClient_GetAllZones(t *testing.T) { } func TestClient_GetAllZones_error(t *testing.T) { - client := mockBuilder(). - Route("GET /dns/domain", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusUnauthorized)). - Build(t) + client := setupTest(t, "GET /dns/domain", http.StatusUnauthorized, "error.json") - _, err := client.GetAllZones(t.Context()) + _, err := client.GetAllZones(context.Background()) 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) + client := setupTest(t, "GET /dns/domain/4/records/SPF_TXT", http.StatusOK, "zone_records_all.json") - groups, err := client.GetAllZoneRecords(t.Context(), 4) + groups, err := client.GetAllZoneRecords(context.Background(), 4) require.NoError(t, err) expected := []ZoneRecord{ @@ -161,141 +180,82 @@ func TestClient_GetAllZoneRecords(t *testing.T) { } 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) + client := setupTest(t, "GET /dns/domain/4/records/SPF_TXT", http.StatusUnauthorized, "error.json") - _, err := client.GetAllZoneRecords(t.Context(), 4) + _, err := client.GetAllZoneRecords(context.Background(), 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) + client := setupTest(t, "DELETE /dns/domain/4/records/SPF_TXT/6", http.StatusOK, "zone_record_delete.json") - err := client.DeleteZoneRecord(t.Context(), 4, 6) + err := client.DeleteZoneRecord(context.Background(), 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) + client := setupTest(t, "DELETE /dns/domain/4/records/SPF_TXT/6", http.StatusUnauthorized, "error.json") - err := client.DeleteZoneRecord(t.Context(), 4, 6) + err := client.DeleteZoneRecord(context.Background(), 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) + client := setupTest(t, "POST /dns/domain/4/records/SPF_TXT/", http.StatusOK, "zone_record_create.json") - 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, - }, - }, - } + record := ZoneRecord{} - err := client.CreateZoneRecord(t.Context(), 4, record) + err := client.CreateZoneRecord(context.Background(), 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) + client := setupTest(t, "POST /dns/domain/4/records/SPF_TXT/", http.StatusUnauthorized, "error.json") record := ZoneRecord{} - err := client.CreateZoneRecord(t.Context(), 4, record) + err := client.CreateZoneRecord(context.Background(), 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) + client := setupTest(t, "POST /dns/domain/4/records/SPF_TXT/", http.StatusBadRequest, "error_bad_request.json") record := ZoneRecord{} - err := client.CreateZoneRecord(t.Context(), 4, record) + err := client.CreateZoneRecord(context.Background(), 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) + client := setupTest(t, "PUT /dns/domain/4/records/SPF_TXT/6/", http.StatusOK, "zone_record_update.json") record := ZoneRecord{ SpfTxtDomainID: 6, ZoneID: 4, } - err := client.UpdateZoneRecord(t.Context(), record) + err := client.UpdateZoneRecord(context.Background(), 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) + client := setupTest(t, "PUT /dns/domain/4/records/SPF_TXT/6/", http.StatusUnauthorized, "error.json") record := ZoneRecord{ SpfTxtDomainID: 6, ZoneID: 4, } - err := client.UpdateZoneRecord(t.Context(), record) + err := client.UpdateZoneRecord(context.Background(), record) require.Error(t, err) require.EqualError(t, err, "[status code: 401] Authentication credentials were not provided.") diff --git a/providers/dns/manageengine/internal/identity.go b/providers/dns/manageengine/internal/identity.go index ec28121e4..66a659188 100644 --- a/providers/dns/manageengine/internal/identity.go +++ b/providers/dns/manageengine/internal/identity.go @@ -9,7 +9,7 @@ import ( const defaultAuthURL = "https://clouddns.manageengine.com/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/manageengine/manageengine.go b/providers/dns/manageengine/manageengine.go index 76b6644c0..f26ae33b5 100644 --- a/providers/dns/manageengine/manageengine.go +++ b/providers/dns/manageengine/manageengine.go @@ -11,7 +11,6 @@ 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/manageengine/internal" ) @@ -76,13 +75,11 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("manageengine: credentials missing") } + client := internal.NewClient(context.Background(), config.ClientID, config.ClientSecret) + return &DNSProvider{ config: config, - client: internal.NewClient( - clientdebug.Wrap( - internal.CreateOAuthClient(context.Background(), config.ClientID, config.ClientSecret), - ), - ), + client: client, }, nil } @@ -195,7 +192,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { // Update the zone record. var values []string - for _, value := range record.Values { if value != info.Value { values = append(values, value) diff --git a/providers/dns/manageengine/manageengine.toml b/providers/dns/manageengine/manageengine.toml index 43a782841..dea92b3e6 100644 --- a/providers/dns/manageengine/manageengine.toml +++ b/providers/dns/manageengine/manageengine.toml @@ -7,7 +7,7 @@ Since = "v4.21.0" Example = ''' MANAGEENGINE_CLIENT_ID="xxx" \ MANAGEENGINE_CLIENT_SECRET="yyy" \ -lego --dns manageengine -d '*.example.com' -d example.com run +lego --email you@example.com --dns manageengine -d '*.example.com' -d example.com run ''' [Configuration] @@ -15,9 +15,10 @@ lego --dns manageengine -d '*.example.com' -d example.com run 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)" + MANAGEENGINE_POLLING_INTERVAL = "Time between DNS propagation check" + MANAGEENGINE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + MANAGEENGINE_TTL = "The TTL of the TXT record used for the DNS challenge" + MANAGEENGINE_HTTP_TIMEOUT = "API request timeout" [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 index 215de68dd..624459be9 100644 --- a/providers/dns/manageengine/manageengine_test.go +++ b/providers/dns/manageengine/manageengine_test.go @@ -50,7 +50,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/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..9b8c41def 100644 --- a/providers/dns/metaname/metaname.go +++ b/providers/dns/metaname/metaname.go @@ -79,7 +79,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 +152,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..142f06639 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 -d '*.example.com' -d example.com 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 index a51233211..82bdcfeb9 100644 --- a/providers/dns/mijnhost/internal/client.go +++ b/providers/dns/mijnhost/internal/client.go @@ -38,7 +38,7 @@ func NewClient(apiKey string) *Client { // ListDomains Retrieve all domains from an account. // https://mijn.host/api/doc/api-3563872 -func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) { +func (c Client) ListDomains(ctx context.Context) ([]Domain, error) { endpoint := c.baseURL.JoinPath("domains") req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) @@ -47,7 +47,6 @@ func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) { } var results Response[DomainData] - err = c.do(req, &results) if err != nil { return nil, err @@ -58,7 +57,7 @@ func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) { // 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) { +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) @@ -67,7 +66,6 @@ func (c *Client) GetRecords(ctx context.Context, domain string) ([]Record, error } var results Response[RecordData] - err = c.do(req, &results) if err != nil { return nil, err @@ -78,7 +76,7 @@ func (c *Client) GetRecords(ctx context.Context, domain string) ([]Record, error // 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 { +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}) @@ -94,7 +92,7 @@ func (c *Client) UpdateRecords(ctx context.Context, domain string, records []Rec 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.apiKey) resp, err := c.HTTPClient.Do(req) @@ -153,7 +151,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/mijnhost/internal/client_test.go b/providers/dns/mijnhost/internal/client_test.go index 208616541..876ca5e1c 100644 --- a/providers/dns/mijnhost/internal/client_test.go +++ b/providers/dns/mijnhost/internal/client_test.go @@ -1,37 +1,72 @@ 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 = "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() +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() - return client, nil - }, - servermock.CheckHeader().WithJSONHeaders(). - With(authorizationHeader, apiKey), - ) + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient(apiKey) + client.baseURL, _ = url.Parse(server.URL) + + return client, mux +} + +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(authorizationHeader) + if auth != 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 TestClient_ListDomains(t *testing.T) { - client := mockBuilder(). - Route("GET /domains", servermock.ResponseFromFixture("list-domains.json")). - Build(t) + client, mux := setupTest(t) - domains, err := client.ListDomains(t.Context()) + mux.HandleFunc("/domains", testHandler("./list-domains.json", http.MethodGet, http.StatusOK)) + + domains, err := client.ListDomains(context.Background()) require.NoError(t, err) expected := []Domain{{ @@ -47,11 +82,11 @@ func TestClient_ListDomains(t *testing.T) { } func TestClient_GetRecords(t *testing.T) { - client := mockBuilder(). - Route("GET /domains/example.com/dns", servermock.ResponseFromFixture("get-dns-records.json")). - Build(t) + client, mux := setupTest(t) - records, err := client.GetRecords(t.Context(), "example.com") + mux.HandleFunc("/domains/example.com/dns", testHandler("./get-dns-records.json", http.MethodGet, http.StatusOK)) + + records, err := client.GetRecords(context.Background(), "example.com") require.NoError(t, err) expected := []Record{ @@ -85,19 +120,10 @@ func TestClient_GetRecords(t *testing.T) { } 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) + client, mux := setupTest(t) - records := []Record{{ - Type: "TXT", - Name: "foo", - Value: "value1", - TTL: 120, - }} + mux.HandleFunc("/domains/example.com/dns", testHandler("./update-dns-records.json", http.MethodPut, http.StatusOK)) - err := client.UpdateRecords(t.Context(), "example.com", records) + err := client.UpdateRecords(context.Background(), "example.com", nil) require.NoError(t, err) } diff --git a/providers/dns/mijnhost/mijnhost.go b/providers/dns/mijnhost/mijnhost.go index adb3e9ce3..32aadfb2d 100644 --- a/providers/dns/mijnhost/mijnhost.go +++ b/providers/dns/mijnhost/mijnhost.go @@ -11,8 +11,8 @@ import ( "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" + "github.com/miekg/dns" ) // Environment variables names. @@ -28,8 +28,6 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const txtType = "TXT" - var _ challenge.ProviderTimeout = (*DNSProvider)(nil) // Config is used to configure the creation of the DNSProvider. @@ -87,12 +85,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { 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, @@ -113,11 +105,9 @@ func (d *DNSProvider) Sequential() 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) - domains, err := d.client.ListDomains(ctx) + domains, err := d.client.ListDomains(context.Background()) if err != nil { return fmt.Errorf("mijnhost: list domains: %w", err) } @@ -127,7 +117,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("mijnhost: find domain: %w", err) } - records, err := d.client.GetRecords(ctx, dom.Domain) + records, err := d.client.GetRecords(context.Background(), dom.Domain) if err != nil { return fmt.Errorf("mijnhost: get records: %w", err) } @@ -138,7 +128,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { } record := internal.Record{ - Type: txtType, + Type: "TXT", Name: subDomain, Value: info.Value, TTL: d.config.TTL, @@ -147,12 +137,12 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // 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)) + return record.Name == subDomain || record.Name == dns01.UnFqdn(info.EffectiveFQDN) }) cleanedRecords = append(cleanedRecords, record) - err = d.client.UpdateRecords(ctx, dom.Domain, cleanedRecords) + err = d.client.UpdateRecords(context.Background(), dom.Domain, cleanedRecords) if err != nil { return fmt.Errorf("mijnhost: update records: %w", err) } @@ -162,11 +152,9 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // 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) + domains, err := d.client.ListDomains(context.Background()) if err != nil { return fmt.Errorf("mijnhost: list domains: %w", err) } @@ -176,16 +164,16 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("mijnhost: find domain: %w", err) } - records, err := d.client.GetRecords(ctx, dom.Domain) + records, err := d.client.GetRecords(context.Background(), 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 + return record.Value == info.Value }) - err = d.client.UpdateRecords(ctx, dom.Domain, cleanedRecords) + err = d.client.UpdateRecords(context.Background(), dom.Domain, cleanedRecords) if err != nil { return fmt.Errorf("mijnhost: update records: %w", err) } @@ -194,7 +182,11 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } func findDomain(domains []internal.Domain, fqdn string) (internal.Domain, error) { - for domain := range dns01.UnFqdnDomainsSeq(fqdn) { + labelIndexes := dns.Split(fqdn) + + for _, index := range labelIndexes { + domain := dns01.UnFqdn(fqdn[index:]) + for _, dom := range domains { if dom.Domain == domain { return dom, nil diff --git a/providers/dns/mijnhost/mijnhost.toml b/providers/dns/mijnhost/mijnhost.toml index 416fdde53..7cea55a18 100644 --- a/providers/dns/mijnhost/mijnhost.toml +++ b/providers/dns/mijnhost/mijnhost.toml @@ -6,18 +6,18 @@ Since = "v4.18.0" Example = ''' MIJNHOST_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns mijnhost -d '*.example.com' -d example.com run +lego --email you@example.com --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)" + MIJNHOST_POLLING_INTERVAL = "Time between DNS propagation check" + MIJNHOST_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + MIJNHOST_SEQUENCE_INTERVAL = "Time between sequential requests" + MIJNHOST_TTL = "The TTL of the TXT record used for the DNS challenge" + MIJNHOST_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://mijn.host/api/doc/" diff --git a/providers/dns/mijnhost/mijnhost_test.go b/providers/dns/mijnhost/mijnhost_test.go index c87ae0a40..a48f84ca8 100644 --- a/providers/dns/mijnhost/mijnhost_test.go +++ b/providers/dns/mijnhost/mijnhost_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) @@ -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/mittwald/internal/client.go b/providers/dns/mittwald/internal/client.go index 2b1564dc1..712caf8df 100644 --- a/providers/dns/mittwald/internal/client.go +++ b/providers/dns/mittwald/internal/client.go @@ -38,7 +38,7 @@ func NewClient(token string) *Client { // ListDomains List Domains. // https://api.mittwald.de/v2/docs/#/Domain/domain-list-domains -func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) { +func (c Client) ListDomains(ctx context.Context) ([]Domain, error) { endpoint := c.baseURL.JoinPath("domains") req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) @@ -47,7 +47,6 @@ func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) { } var result []Domain - err = c.do(req, &result) if err != nil { return nil, err @@ -58,7 +57,7 @@ func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) { // 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) { +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) @@ -67,7 +66,6 @@ func (c *Client) GetDNSZone(ctx context.Context, zoneID string) (*DNSZone, error } result := &DNSZone{} - err = c.do(req, result) if err != nil { return nil, err @@ -78,7 +76,7 @@ func (c *Client) GetDNSZone(ctx context.Context, zoneID string) (*DNSZone, error // 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) { +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) @@ -87,7 +85,6 @@ func (c *Client) ListDNSZones(ctx context.Context, projectID string) ([]DNSZone, } var result []DNSZone - err = c.do(req, &result) if err != nil { return nil, err @@ -98,7 +95,7 @@ func (c *Client) ListDNSZones(ctx context.Context, projectID string) ([]DNSZone, // 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) { +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) @@ -107,7 +104,6 @@ func (c *Client) CreateDNSZone(ctx context.Context, zone CreateDNSZoneRequest) ( } result := &DNSZone{} - err = c.do(req, result) if err != nil { return nil, err @@ -118,7 +114,7 @@ func (c *Client) CreateDNSZone(ctx context.Context, zone CreateDNSZoneRequest) ( // 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 { +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) @@ -131,7 +127,7 @@ func (c *Client) UpdateTXTRecord(ctx context.Context, zoneID string, record TXTR // 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 { +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) @@ -142,7 +138,7 @@ func (c *Client) DeleteDNSZone(ctx context.Context, zoneID string) error { 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(authorizationHeader, "Bearer "+c.token) resp, err := c.HTTPClient.Do(req) @@ -201,7 +197,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/mittwald/internal/client_test.go b/providers/dns/mittwald/internal/client_test.go index e57c80f7a..63fc52004 100644 --- a/providers/dns/mittwald/internal/client_test.go +++ b/providers/dns/mittwald/internal/client_test.go @@ -1,36 +1,75 @@ 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, 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) + + mux.HandleFunc(pattern, handler) + + client := NewClient("secret") + 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 + } + + auth := req.Header.Get(authorizationHeader) + if auth != "Bearer secret" { + http.Error(rw, fmt.Sprintf("invalid API Token: %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 + } + } } func TestClient_ListDomains(t *testing.T) { - client := mockBuilder(). - Route("GET /domains", servermock.ResponseFromFixture("domain-list-domains.json")). - Build(t) + client := setupTest(t, "/domains", testHandler(http.MethodGet, http.StatusOK, "domain-list-domains.json")) - domains, err := client.ListDomains(t.Context()) + domains, err := client.ListDomains(context.Background()) require.NoError(t, err) require.Len(t, domains, 1) @@ -45,22 +84,16 @@ func TestClient_ListDomains(t *testing.T) { } func TestClient_ListDomains_error(t *testing.T) { - client := mockBuilder(). - Route("GET /domains", - servermock.ResponseFromFixture("error-client.json"). - WithStatusCode(http.StatusBadRequest)). - Build(t) + client := setupTest(t, "/domains", testHandler(http.MethodGet, http.StatusBadRequest, "error-client.json")) - _, err := client.ListDomains(t.Context()) + _, err := client.ListDomains(context.Background()) 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) + client := setupTest(t, "/projects/my-project-id/dns-zones", testHandler(http.MethodGet, http.StatusOK, "dns-list-dns-zones.json")) - zones, err := client.ListDNSZones(t.Context(), "my-project-id") + zones, err := client.ListDNSZones(context.Background(), "my-project-id") require.NoError(t, err) require.Len(t, zones, 1) @@ -77,11 +110,9 @@ func TestClient_ListDNSZones(t *testing.T) { } func TestClient_GetDNSZone(t *testing.T) { - client := mockBuilder(). - Route("GET /dns-zones/my-zone-id", servermock.ResponseFromFixture("dns-get-dns-zone.json")). - Build(t) + client := setupTest(t, "/dns-zones/my-zone-id", testHandler(http.MethodGet, http.StatusOK, "dns-get-dns-zone.json")) - zone, err := client.GetDNSZone(t.Context(), "my-zone-id") + zone, err := client.GetDNSZone(context.Background(), "my-zone-id") require.NoError(t, err) expected := &DNSZone{ @@ -96,18 +127,14 @@ func TestClient_GetDNSZone(t *testing.T) { } 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) + client := setupTest(t, "/dns-zones", testHandler(http.MethodPost, http.StatusCreated, "dns-create-dns-zone.json")) request := CreateDNSZoneRequest{ Name: "test", ParentZoneID: "my-parent-zone-id", } - zone, err := client.CreateDNSZone(t.Context(), request) + zone, err := client.CreateDNSZone(context.Background(), request) require.NoError(t, err) expected := &DNSZone{ @@ -118,12 +145,7 @@ func TestClient_CreateDNSZone(t *testing.T) { } 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) + client := setupTest(t, "/dns-zones/my-zone-id/record-sets/txt", testHandler(http.MethodPut, http.StatusNoContent, "")) record := TXTRecord{ Settings: Settings{ @@ -132,27 +154,20 @@ func TestClient_UpdateTXTRecord(t *testing.T) { Entries: []string{"txt"}, } - err := client.UpdateTXTRecord(t.Context(), "my-zone-id", record) + err := client.UpdateTXTRecord(context.Background(), "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) + client := setupTest(t, "/dns-zones/my-zone-id", testHandler(http.MethodDelete, http.StatusOK, "")) - err := client.DeleteDNSZone(t.Context(), "my-zone-id") + err := client.DeleteDNSZone(context.Background(), "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) + client := setupTest(t, "/dns-zones/my-zone-id", testHandler(http.MethodDelete, http.StatusInternalServerError, "error.json")) - err := client.DeleteDNSZone(t.Context(), "my-zone-id") + err := client.DeleteDNSZone(context.Background(), "my-zone-id") assert.EqualError(t, err, "[status code 500] InternalServerError: Something went wrong") } diff --git a/providers/dns/mittwald/internal/types.go b/providers/dns/mittwald/internal/types.go index 86cdf065c..df10ab293 100644 --- a/providers/dns/mittwald/internal/types.go +++ b/providers/dns/mittwald/internal/types.go @@ -1,9 +1,6 @@ package internal -import ( - "fmt" - "strings" -) +import "fmt" // https://api.mittwald.de/v2/docs/#/Domain/domain-list-domains @@ -39,7 +36,7 @@ type NewDNSZone struct { // https://api.mittwald.de/v2/docs/#/Domain/dns-update-record-set type TXTRecord struct { - Settings Settings `json:"settings"` + Settings Settings `json:"settings,omitempty"` Entries []string `json:"entries,omitempty"` } @@ -61,25 +58,23 @@ type APIError struct { } func (a APIError) Error() string { - msg := new(strings.Builder) - - _, _ = fmt.Fprintf(msg, "%s: %s", a.Type, a.Message) + msg := fmt.Sprintf("%s: %s", a.Type, a.Message) if len(a.ValidationErrors) > 0 { for _, validationError := range a.ValidationErrors { - _, _ = fmt.Fprintf(msg, " [%s: %s (%s, %s)]", + msg += fmt.Sprintf(" [%s: %s (%s, %s)]", validationError.Type, validationError.Message, validationError.Path, validationError.Context.Format) } } - return msg.String() + return msg } type ValidationError struct { Message string `json:"message,omitempty"` Path string `json:"path,omitempty"` Type string `json:"type,omitempty"` - Context ValidationErrorContext `json:"context"` + Context ValidationErrorContext `json:"context,omitempty"` } type ValidationErrorContext struct { diff --git a/providers/dns/mittwald/mittwald.go b/providers/dns/mittwald/mittwald.go index dcd882482..47c62be52 100644 --- a/providers/dns/mittwald/mittwald.go +++ b/providers/dns/mittwald/mittwald.go @@ -12,8 +12,8 @@ import ( "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" + "github.com/miekg/dns" ) // Environment variables names. @@ -93,17 +93,9 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { 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, + client: internal.NewClient(config.Token), zoneIDs: map[string]string{}, }, nil } @@ -158,7 +150,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("mittwald: unknown zone ID for '%s'", info.EffectiveFQDN) } @@ -170,10 +161,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("mittwald: update/delete TXT record: %w", err) } - d.zoneIDsMu.Lock() - delete(d.zoneIDs, token) - d.zoneIDsMu.Unlock() - return nil } @@ -225,7 +212,11 @@ func (d *DNSProvider) getOrCreateZone(ctx context.Context, fqdn string) (*intern } func findDomain(domains []internal.Domain, fqdn string) (internal.Domain, error) { - for domain := range dns01.UnFqdnDomainsSeq(fqdn) { + labelIndexes := dns.Split(fqdn) + + for _, index := range labelIndexes { + domain := dns01.UnFqdn(fqdn[index:]) + for _, dom := range domains { if dom.Domain == domain { return dom, nil @@ -237,7 +228,11 @@ func findDomain(domains []internal.Domain, fqdn string) (internal.Domain, error) } func findZone(zones []internal.DNSZone, fqdn string) (internal.DNSZone, error) { - for domain := range dns01.UnFqdnDomainsSeq(fqdn) { + labelIndexes := dns.Split(fqdn) + + for _, index := range labelIndexes { + domain := dns01.UnFqdn(fqdn[index:]) + for _, zon := range zones { if zon.Domain == domain { return zon, nil diff --git a/providers/dns/mittwald/mittwald.toml b/providers/dns/mittwald/mittwald.toml index 36a9f6c16..7df9797b6 100644 --- a/providers/dns/mittwald/mittwald.toml +++ b/providers/dns/mittwald/mittwald.toml @@ -6,18 +6,18 @@ Since = "v1.48.0" Example = ''' MITTWALD_TOKEN=my-token \ -lego --dns mittwald -d '*.example.com' -d example.com run +lego --email you@example.com --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)" + MITTWALD_POLLING_INTERVAL = "Time between DNS propagation check" + MITTWALD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + MITTWALD_TTL = "The TTL of the TXT record used for the DNS challenge" + MITTWALD_HTTP_TIMEOUT = "API request timeout" + MITTWALD_SEQUENCE_INTERVAL = "Time between sequential requests" [Links] API = "https://api.mittwald.de/v2/docs/" diff --git a/providers/dns/mittwald/mittwald_test.go b/providers/dns/mittwald/mittwald_test.go index 6a6599536..d8cbdb263 100644 --- a/providers/dns/mittwald/mittwald_test.go +++ b/providers/dns/mittwald/mittwald_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) @@ -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/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..ec1aca357 100644 --- a/providers/dns/mydnsjp/mydnsjp.go +++ b/providers/dns/mydnsjp/mydnsjp.go @@ -11,7 +11,6 @@ import ( "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" ) @@ -42,7 +41,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 +79,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 +100,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("mydnsjp: %w", err) } - return nil } @@ -122,6 +112,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..d462e9537 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 -d '*.example.com' -d example.com 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..ae8f72d33 100644 --- a/providers/dns/mythicbeasts/mythicbeasts.go +++ b/providers/dns/mythicbeasts/mythicbeasts.go @@ -12,7 +12,6 @@ import ( "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" ) @@ -88,7 +87,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 +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/mythicbeasts/mythicbeasts.toml b/providers/dns/mythicbeasts/mythicbeasts.toml index cada3041d..86d69d017 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 -d '*.example.com' -d example.com 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..f410fa5a3 100644 --- a/providers/dns/namecheap/namecheap.go +++ b/providers/dns/namecheap/namecheap.go @@ -14,7 +14,6 @@ import ( "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" ) @@ -73,11 +72,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 +117,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if err != nil { return nil, fmt.Errorf("namecheap: %w", err) } - config.ClientIP = clientIP } @@ -130,8 +127,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{config: config, client: client}, nil } @@ -176,7 +171,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("namecheap: %w", err) } - return nil } @@ -196,11 +190,8 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } // 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" { found = true @@ -217,7 +208,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("namecheap: %w", err) } - return nil } diff --git a/providers/dns/namecheap/namecheap.toml b/providers/dns/namecheap/namecheap.toml index b0f92a1bd..ef2ef53c4 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 -d '*.example.com' -d example.com 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..01f87aaf0 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, _ := newPseudoRecord(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 { @@ -152,7 +205,6 @@ 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, "") 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..5b2bbaf21 100644 --- a/providers/dns/namedotcom/namedotcom.go +++ b/providers/dns/namedotcom/namedotcom.go @@ -10,8 +10,7 @@ import ( "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" ) // Environment variables names. @@ -98,12 +97,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 +110,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 +121,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 +142,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 +150,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 +178,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..768164cf8 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 -d '*.example.com' -d example.com 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..f76c8549e 100644 --- a/providers/dns/namesilo/namesilo.go +++ b/providers/dns/namesilo/namesilo.go @@ -2,7 +2,6 @@ package namesilo import ( - "context" "errors" "fmt" "time" @@ -10,7 +9,6 @@ import ( "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" ) @@ -81,15 +79,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 +103,7 @@ 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.client.DnsAddRecord(&namesilo.DnsAddRecordParams{ Domain: zoneName, Type: "TXT", Host: subdomain, @@ -118,14 +113,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 +127,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) } @@ -147,7 +139,7 @@ func (d *DNSProvider) CleanUp(domain, _, keyAuth string) 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}) + _, err := d.client.DnsDeleteRecord(&namesilo.DnsDeleteRecordParams{Domain: zoneName, ID: r.RecordID}) if err != nil { return fmt.Errorf("namesilo: %w", err) } diff --git a/providers/dns/namesilo/namesilo.toml b/providers/dns/namesilo/namesilo.toml index 113ddb5c5..991e78fcc 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 -d '*.example.com' -d example.com 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..464ac35d0 100644 --- a/providers/dns/nearlyfreespeech/nearlyfreespeech.go +++ b/providers/dns/nearlyfreespeech/nearlyfreespeech.go @@ -11,7 +11,6 @@ import ( "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" ) @@ -93,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, diff --git a/providers/dns/nearlyfreespeech/nearlyfreespeech.toml b/providers/dns/nearlyfreespeech/nearlyfreespeech.toml index 3a1e25942..985df6cba 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 -d '*.example.com' -d example.com 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..b0ef4a2bf 100644 --- a/providers/dns/netcup/netcup.go +++ b/providers/dns/netcup/netcup.go @@ -13,7 +13,6 @@ import ( "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,9 +24,7 @@ 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" @@ -40,17 +37,16 @@ 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{ + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 15*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 30*time.Second), HTTPClient: &http.Client{ @@ -93,11 +89,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 +111,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 +120,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 +158,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..0954d07d6 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 -d '*.example.com' -d example.com 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..1d4c78f4f 100644 --- a/providers/dns/netlify/netlify.go +++ b/providers/dns/netlify/netlify.go @@ -13,7 +13,6 @@ import ( "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" ) @@ -85,11 +84,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 +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("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..1191c6beb 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 -d '*.example.com' -d example.com 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..f9307d8c1 100644 --- a/providers/dns/nicmanager/nicmanager.go +++ b/providers/dns/nicmanager/nicmanager.go @@ -12,7 +12,6 @@ import ( "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 +24,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" @@ -86,7 +85,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 +128,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 +188,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..7be44deb8 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 -d '*.example.com' -d example.com 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 -d '*.example.com' -d example.com 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..4469a1f78 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 @@ -131,7 +129,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 +148,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 +170,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..e73333c52 100644 --- a/providers/dns/nifcloud/nifcloud.go +++ b/providers/dns/nifcloud/nifcloud.go @@ -9,12 +9,10 @@ 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" ) @@ -95,8 +93,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 +107,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 +133,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 +170,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 +179,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..9966ce882 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 -d '*.example.com' -d example.com 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..b08ce69de 100644 --- a/providers/dns/njalla/njalla.go +++ b/providers/dns/njalla/njalla.go @@ -12,7 +12,6 @@ import ( "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" ) @@ -91,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, @@ -148,7 +145,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..a7e46c02d 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 -d '*.example.com' -d example.com 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..1fdc8b87d 100644 --- a/providers/dns/nodion/nodion.go +++ b/providers/dns/nodion/nodion.go @@ -12,7 +12,6 @@ import ( "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" ) @@ -94,8 +93,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 +169,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 +204,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..5bf2e1df1 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 you@example.com --dns nodion -d '*.example.com' -d example.com 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..c3bf168cb 100644 --- a/providers/dns/ns1/ns1.go +++ b/providers/dns/ns1/ns1.go @@ -11,7 +11,6 @@ import ( "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" ) @@ -81,12 +80,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 +141,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..9aeb0841e 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 -d '*.example.com' -d example.com 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..535c691ba 100644 --- a/providers/dns/oraclecloud/oraclecloud.go +++ b/providers/dns/oraclecloud/oraclecloud.go @@ -11,32 +11,22 @@ import ( "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 +34,12 @@ 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 +53,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 +66,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 +98,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 +168,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..70b776554 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 -d '*.example.com' -d example.com 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..3569e6343 100644 --- a/providers/dns/otc/otc.go +++ b/providers/dns/otc/otc.go @@ -11,7 +11,6 @@ import ( "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" ) @@ -24,7 +23,6 @@ const ( EnvPassword = envNamespace + "PASSWORD" EnvProjectName = envNamespace + "PROJECT_NAME" EnvIdentityEndpoint = envNamespace + "IDENTITY_ENDPOINT" - EnvPrivateZone = envNamespace + "PRIVATE_ZONE" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" @@ -42,13 +40,11 @@ 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 @@ -69,12 +65,10 @@ func NewDefaultConfig() *Config { 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), @@ -131,8 +125,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 +144,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 +181,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..547a1a47d 100644 --- a/providers/dns/ovh/ovh.go +++ b/providers/dns/ovh/ovh.go @@ -11,7 +11,6 @@ import ( "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" ) @@ -84,6 +83,10 @@ type Config struct { 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 +99,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 } @@ -192,7 +190,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 +197,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 +217,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 +237,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) @@ -263,10 +257,8 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { } func newClient(config *Config) (*ovh.Client, error) { - var ( - client *ovh.Client - err error - ) + var client *ovh.Client + var err error switch { case config.hasAppKeyAuth(): @@ -285,11 +277,5 @@ func newClient(config *Config) (*ovh.Client, error) { client.UserAgent = useragent.Get() - if config.HTTPClient != nil { - client.Client = config.HTTPClient - } - - client.Client = clientdebug.Wrap(client.Client) - return client, nil } diff --git a/providers/dns/ovh/ovh.toml b/providers/dns/ovh/ovh.toml index abf22bd7a..cbdcb43ae 100644 --- a/providers/dns/ovh/ovh.toml +++ b/providers/dns/ovh/ovh.toml @@ -11,20 +11,20 @@ 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 +lego --email you@example.com --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 -d '*.example.com' -d example.com 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 -d '*.example.com' -d example.com run ''' Additional = ''' @@ -76,10 +76,10 @@ Both authentication methods cannot be used at the same time. 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..f070f2e85 100644 --- a/providers/dns/ovh/ovh_test.go +++ b/providers/dns/ovh/ovh_test.go @@ -162,7 +162,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) @@ -202,11 +201,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: new client: unknown endpoint '', consider checking 'Endpoints' list or using an URL", }, { desc: "application key: invalid api endpoint", @@ -239,10 +239,11 @@ func TestNewDNSProviderConfig(t *testing.T) { clientSecret: "C", }, { - desc: "oauth2: default api endpoint", + desc: "oauth2: missing api endpoint", apiEndpoint: "", clientID: "B", clientSecret: "C", + expected: "ovh: new client: unknown endpoint '', consider checking 'Endpoints' list or using an URL", }, { desc: "oauth2: invalid api endpoint", @@ -316,7 +317,6 @@ func TestNewDNSProviderConfig(t *testing.T) { // 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 { @@ -356,7 +356,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -370,7 +369,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..07bc663f1 100644 --- a/providers/dns/pdns/pdns.go +++ b/providers/dns/pdns/pdns.go @@ -7,14 +7,12 @@ 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" ) @@ -104,12 +102,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 +120,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 +127,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 +143,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 +189,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..81158c444 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 -d '*.example.com' -d example.com 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..b7a7ebf77 100644 --- a/providers/dns/plesk/plesk.go +++ b/providers/dns/plesk/plesk.go @@ -13,7 +13,6 @@ import ( "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" ) @@ -108,8 +107,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 +160,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 +169,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..3a67065d6 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 -d '*.example.com' -d example.com 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..44bf1857b 100644 --- a/providers/dns/porkbun/porkbun.go +++ b/providers/dns/porkbun/porkbun.go @@ -13,7 +13,6 @@ import ( "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" ) @@ -101,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, @@ -154,7 +151,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 +167,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..91b0b1329 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 -d '*.example.com' -d example.com 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..b9ce8f6e3 100644 --- a/providers/dns/rackspace/rackspace.go +++ b/providers/dns/rackspace/rackspace.go @@ -11,7 +11,6 @@ import ( "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" ) @@ -99,7 +98,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 +118,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..ae0b0fca4 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 -d '*.example.com' -d example.com 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 index 595b39f29..3d99bd9be 100644 --- a/providers/dns/rainyun/internal/client.go +++ b/providers/dns/rainyun/internal/client.go @@ -84,7 +84,6 @@ func (c *Client) ListRecords(ctx context.Context, domainID int) ([]Record, error } var recordData APIResponse[Record] - err = c.do(req, &recordData) if err != nil { return nil, err @@ -174,7 +173,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/rainyun/internal/client_test.go b/providers/dns/rainyun/internal/client_test.go index 8246001af..ee6477c0c 100644 --- a/providers/dns/rainyun/internal/client_test.go +++ b/providers/dns/rainyun/internal/client_test.go @@ -1,41 +1,61 @@ package internal import ( + "context" + "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") - if err != nil { - return nil, err - } +func setupTest(t *testing.T, pattern 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()) + mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { + if filename == "" { + rw.WriteHeader(status) + 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(status) + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + client, err := NewClient("secret") + require.NoError(t, err) + + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client } 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) + client := setupTest(t, "GET /domain", http.StatusOK, "domains.json") - domains, err := client.ListDomains(t.Context()) + domains, err := client.ListDomains(context.Background()) require.NoError(t, err) expected := []Domain{ @@ -47,28 +67,18 @@ func TestClient_ListDomains(t *testing.T) { } func TestClient_ListDomains_error(t *testing.T) { - client := mockBuilder(). - Route("GET /domain", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusForbidden)). - Build(t) + client := setupTest(t, "GET /domain", http.StatusForbidden, "error.json") - _, err := client.ListDomains(t.Context()) + _, err := client.ListDomains(context.Background()) 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) + client := setupTest(t, "GET /domain/123/dns", http.StatusOK, "records.json") - records, err := client.ListRecords(t.Context(), 123) + records, err := client.ListRecords(context.Background(), 123) require.NoError(t, err) expected := []Record{ @@ -94,22 +104,16 @@ func TestClient_ListRecords(t *testing.T) { } func TestClient_ListRecords_error(t *testing.T) { - client := mockBuilder(). - Route("GET /domain/123/dns", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusForbidden)). - Build(t) + client := setupTest(t, "GET /domain/123/dns", http.StatusForbidden, "error.json") - _, err := client.ListRecords(t.Context(), 123) + _, err := client.ListRecords(context.Background(), 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) + client := setupTest(t, "POST /domain/123/dns", http.StatusOK, "") record := Record{ Host: "_acme-challenge.foo.example.com", @@ -119,16 +123,12 @@ func TestClient_AddRecord(t *testing.T) { Value: "foo", } - err := client.AddRecord(t.Context(), 123, record) + err := client.AddRecord(context.Background(), 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) + client := setupTest(t, "POST /domain/123/dns", http.StatusForbidden, "error.json") record := Record{ Host: "_acme-challenge.foo.example.com", @@ -138,29 +138,23 @@ func TestClient_AddRecord_error(t *testing.T) { Value: "foo", } - err := client.AddRecord(t.Context(), 123, record) + err := client.AddRecord(context.Background(), 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) + client := setupTest(t, "DELETE /domain/123/dns", http.StatusOK, "") - 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 /domain/123/dns", - servermock.ResponseFromFixture("error.json"). - WithStatusCode(http.StatusForbidden)). - Build(t) + client := setupTest(t, "DELETE /domain/123/dns", http.StatusForbidden, "error.json") - err := client.DeleteRecord(t.Context(), 123, 456) + err := client.DeleteRecord(context.Background(), 123, 456) require.Error(t, err) assert.EqualError(t, err, "30039: 密钥认证错误或已失效") diff --git a/providers/dns/rainyun/rainyun.go b/providers/dns/rainyun/rainyun.go index a4d1c4035..43ef9cb1b 100644 --- a/providers/dns/rainyun/rainyun.go +++ b/providers/dns/rainyun/rainyun.go @@ -12,7 +12,6 @@ import ( "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" ) @@ -86,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/rainyun/rainyun.toml b/providers/dns/rainyun/rainyun.toml index fe2b3c07d..ea12b475f 100644 --- a/providers/dns/rainyun/rainyun.toml +++ b/providers/dns/rainyun/rainyun.toml @@ -6,17 +6,17 @@ Since = "v4.21.0" Example = ''' RAINYUN_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns rainyun -d '*.example.com' -d example.com run +lego --email you@example.com --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)" + RAINYUN_POLLING_INTERVAL = "Time between DNS propagation check" + RAINYUN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + RAINYUN_TTL = "The TTL of the TXT record used for the DNS challenge" + RAINYUN_HTTP_TIMEOUT = "API request timeout" [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 index d27d47e81..d0048e5d0 100644 --- a/providers/dns/rainyun/rainyun_test.go +++ b/providers/dns/rainyun/rainyun_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/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..c19e6e5b8 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) } 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..c88caefe4 100644 --- a/providers/dns/rcodezero/rcodezero.go +++ b/providers/dns/rcodezero/rcodezero.go @@ -11,7 +11,6 @@ import ( "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" ) @@ -42,7 +41,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 +86,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..7ab451e5f 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 -d '*.example.com' -d example.com 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 index 85aac92e5..6a8ccee98 100644 --- a/providers/dns/regfish/regfish.go +++ b/providers/dns/regfish/regfish.go @@ -11,7 +11,6 @@ import ( "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" ) @@ -85,15 +84,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { 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, @@ -132,7 +122,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("regfish: unknown record ID for '%s'", info.EffectiveFQDN) } diff --git a/providers/dns/regfish/regfish.toml b/providers/dns/regfish/regfish.toml index fbaacbde4..fbc4bdd70 100644 --- a/providers/dns/regfish/regfish.toml +++ b/providers/dns/regfish/regfish.toml @@ -6,17 +6,17 @@ Since = "v4.20.0" Example = ''' REGFISH_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ -lego --dns regfish -d '*.example.com' -d example.com run +lego --email you@example.com --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)" + REGFISH_POLLING_INTERVAL = "Time between DNS propagation check" + REGFISH_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + REGFISH_TTL = "The TTL of the TXT record used for the DNS challenge" + REGFISH_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://regfish.readme.io/" diff --git a/providers/dns/regfish/regfish_test.go b/providers/dns/regfish/regfish_test.go index 6613bd508..80928048f 100644 --- a/providers/dns/regfish/regfish_test.go +++ b/providers/dns/regfish/regfish_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/regru/internal/client.go b/providers/dns/regru/internal/client.go index b0b86d567..7ce633b05 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,7 +73,7 @@ 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...) inputData, err := json.Marshal(request) @@ -111,7 +111,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 +123,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..1501863bd 100644 --- a/providers/dns/regru/regru.go +++ b/providers/dns/regru/regru.go @@ -12,7 +12,6 @@ import ( "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" ) @@ -98,8 +97,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..16d8e4e3a 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 -d '*.example.com' -d example.com 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/rfc2136.go b/providers/dns/rfc2136/rfc2136.go index 2c4fe7aeb..d533f4d16 100644 --- a/providers/dns/rfc2136/rfc2136.go +++ b/providers/dns/rfc2136/rfc2136.go @@ -58,8 +58,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), } @@ -131,7 +131,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { config.TSIGSecret = "" } else { // zonename must be in canonical form (lowercase, fqdn, see RFC 4034 Section 6.2) - config.TSIGKey = dns.CanonicalName(config.TSIGKey) + config.TSIGKey = strings.ToLower(dns.Fqdn(config.TSIGKey)) } if config.TSIGAlgorithm == "" { @@ -171,7 +171,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 +182,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 +193,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. @@ -230,7 +228,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..df313fde7 100644 --- a/providers/dns/rfc2136/rfc2136.toml +++ b/providers/dns/rfc2136/rfc2136.toml @@ -9,7 +9,7 @@ RFC2136_NAMESERVER=127.0.0.1 \ RFC2136_TSIG_KEY=example.com \ 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 -d '*.example.com' -d example.com run ## --- @@ -17,7 +17,7 @@ keyname=example.com; keyfile=example.com.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 +lego --email you@example.com --dns rfc2136 -d '*.example.com' -d example.com run ''' [Configuration] @@ -28,11 +28,11 @@ lego --dns rfc2136 -d '*.example.com' -d example.com run 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..80fdc69cb 100644 --- a/providers/dns/rfc2136/rfc2136_test.go +++ b/providers/dns/rfc2136/rfc2136_test.go @@ -2,21 +2,24 @@ 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 @@ -84,7 +87,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,16 +163,39 @@ func TestNewDNSProviderConfig(t *testing.T) { } } -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 +204,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 +247,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..9051d0add 100644 --- a/providers/dns/rimuhosting/rimuhosting.go +++ b/providers/dns/rimuhosting/rimuhosting.go @@ -2,6 +2,7 @@ package rimuhosting import ( + "context" "errors" "fmt" "net/http" @@ -28,12 +29,19 @@ const ( 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 +52,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 +76,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 +125,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..4b4fa5ea7 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 -d '*.example.com' -d example.com 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..8246cd0ad 100644 --- a/providers/dns/route53/route53.go +++ b/providers/dns/route53/route53.go @@ -17,7 +17,6 @@ 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" @@ -36,7 +35,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" @@ -60,7 +58,6 @@ type Config struct { MaxRetries int AssumeRoleArn string ExternalID string - PrivateZone bool WaitForRecordSetsChanged bool @@ -78,7 +75,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,7 +151,6 @@ 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 { found = true @@ -201,7 +196,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } var nonLegoRecords []awstypes.ResourceRecord - for _, record := range existingRecords { if ptr.Deref(record.Value) != `"`+info.Value+`"` { nonLegoRecords = append(nonLegoRecords, record) @@ -252,22 +246,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", ptr.Deref(changeID)) + }) } return nil @@ -314,17 +304,15 @@ 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 { + if !hostedZone.Config.PrivateZone && ptr.Deref(hostedZone.Name) == authZone { hostedZoneID = ptr.Deref(hostedZone.Id) break } @@ -354,10 +342,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 }) }) diff --git a/providers/dns/route53/route53.toml b/providers/dns/route53/route53.toml index 607d9ef31..53c1d61d1 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 --email you@example.com --dns route53 -d '*.example.com' -d example.com 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..9467fb77a 100644 --- a/providers/dns/route53/route53_integration_test.go +++ b/providers/dns/route53/route53_integration_test.go @@ -1,6 +1,7 @@ package route53 import ( + "context" "testing" "github.com/aws/aws-sdk-go-v2/aws" @@ -28,7 +29,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 +43,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{ diff --git a/providers/dns/route53/route53_test.go b/providers/dns/route53/route53_test.go index 41ed824bc..1c835ac37 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,31 @@ var envTest = tester.NewEnvTest( WithDomain(envDomain). WithLiveTestRequirements(EnvAccessKeyID, EnvSecretAccessKey, EnvRegion, envDomain) +func makeTestProvider(t *testing.T, serverURL string) *DNSProvider { + t.Helper() + + cfg := aws.Config{ + Credentials: credentials.NewStaticCredentialsProvider("abc", "123", " "), + Region: "mock-region", + BaseEndpoint: aws.String(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 +74,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 +86,6 @@ func Test_loadRegion_FromEnv(t *testing.T) { func Test_getHostedZoneID_FromEnv(t *testing.T) { defer envTest.RestoreEnv() - envTest.ClearEnv() expectedZoneID := "zoneID" @@ -84,8 +95,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 +142,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 +154,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 +263,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..5066db59f 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 ( @@ -12,9 +12,7 @@ import ( "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. @@ -75,7 +73,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 +89,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 +106,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 +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("safedns: unknown record ID for '%s'", info.EffectiveFQDN) } diff --git a/providers/dns/safedns/safedns.toml b/providers/dns/safedns/safedns.toml index f387f2535..11b2a289c 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 -d '*.example.com' -d example.com 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..498f76c42 100644 --- a/providers/dns/sakuracloud/sakuracloud.go +++ b/providers/dns/sakuracloud/sakuracloud.go @@ -2,21 +2,17 @@ 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" ) @@ -102,13 +98,13 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { Options: &client.Options{ AccessToken: config.Token, AccessTokenSecret: config.Secret, - HttpClient: clientdebug.Wrap(config.HTTPClient), + HttpClient: config.HTTPClient, UserAgent: fmt.Sprintf("%s %s", iaas.DefaultUserAgent, useragent.Get()), }, } return &DNSProvider{ - client: iaas.NewDNSOp(newCallerWithOptions(api.MergeOptions(defaultOption, options))), + client: iaas.NewDNSOp(api.NewCallerWithOptions(api.MergeOptions(defaultOption, options))), config: config, }, nil } @@ -117,7 +113,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 +125,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 +138,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..f86f215e5 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 -d '*.example.com' -d example.com 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..5976e77a2 100644 --- a/providers/dns/scaleway/scaleway.go +++ b/providers/dns/scaleway/scaleway.go @@ -5,7 +5,6 @@ package scaleway import ( "errors" "fmt" - "net/http" "strconv" "strings" "time" @@ -13,7 +12,6 @@ import ( "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" @@ -34,7 +32,6 @@ const ( EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) const ( @@ -50,14 +47,12 @@ 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 +62,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), - }, } } @@ -115,10 +107,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { scw.WithUserAgent(useragent.Get()), } - if config.HTTPClient != nil { - configuration = append(configuration, scw.WithHTTPClient(clientdebug.Wrap(config.HTTPClient))) - } - if config.ProjectID != "" { configuration = append(configuration, scw.WithDefaultProjectID(config.ProjectID)) } diff --git a/providers/dns/scaleway/scaleway.toml b/providers/dns/scaleway/scaleway.toml index 8b556e8b1..a13a34d22 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 -d '*.example.com' -d example.com 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..744523230 100644 --- a/providers/dns/selectel/selectel.go +++ b/providers/dns/selectel/selectel.go @@ -4,9 +4,11 @@ package selectel import ( + "context" "errors" "fmt" "net/http" + "net/url" "time" "github.com/go-acme/lego/v4/challenge" @@ -28,18 +30,27 @@ 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 = 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 +59,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 +83,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..a37565d4d 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 -d '*.example.com' -d example.com 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..f638b0a3f 100644 --- a/providers/dns/selectelv2/selectelv2.go +++ b/providers/dns/selectelv2/selectelv2.go @@ -11,25 +11,20 @@ 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" selectelapi "github.com/selectel/domains-go/pkg/v2" - "github.com/selectel/go-selvpcclient/v4/selvpcclient" + "github.com/selectel/go-selvpcclient/v3/selvpcclient" "golang.org/x/net/idna" ) 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" @@ -38,12 +33,7 @@ const ( ) const ( - defaultBaseURL = "https://api.selectel.ru/domains/v2" - defaultAuthRegion = "ru-1" - defaultAuthURL = "https://cloud.api.selcloud.ru/identity/v3/" -) - -const ( + defaultBaseURL = "https://api.selectel.ru/domains/v2" defaultTTL = 60 defaultPropagationTimeout = 120 * time.Second defaultPollingInterval = 5 * time.Second @@ -56,15 +46,11 @@ 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 +60,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, defaultBaseURL), TTL: env.GetOrDefaultInt(EnvTTL, defaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, defaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, defaultPollingInterval), @@ -94,7 +77,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 +85,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 +105,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 == "" { @@ -135,22 +117,22 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { useragent.SetHeader(headers) return &DNSProvider{ - baseClient: selectelapi.NewClient(config.BaseURL, clientdebug.Wrap(config.HTTPClient), headers), + baseClient: selectelapi.NewClient(config.BaseURL, 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 +153,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 +176,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 +220,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 +230,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) @@ -288,7 +266,7 @@ 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(unicodeName) { return zone, nil } } @@ -297,10 +275,10 @@ func (w *clientWrapper) getZone(ctx context.Context, name string) (*selectelapi. return nil, fmt.Errorf("zone '%s' for challenge has not been found", name) } - // 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) { @@ -317,7 +295,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(unicodeName) { return rrset, nil } } diff --git a/providers/dns/selectelv2/selectelv2.toml b/providers/dns/selectelv2/selectelv2.toml index 480c7756e..4c06949f4 100644 --- a/providers/dns/selectelv2/selectelv2.toml +++ b/providers/dns/selectelv2/selectelv2.toml @@ -9,7 +9,7 @@ SELECTELV2_USERNAME=trex \ SELECTELV2_PASSWORD=xxxxx \ SELECTELV2_ACCOUNT_ID=1234567 \ SELECTELV2_PROJECT_ID=111a11111aaa11aa1a11aaa11111aa1a \ -lego --dns selectelv2 -d '*.example.com' -d example.com run +lego --email you@example.com --dns selectelv2 -d '*.example.com' -d example.com 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_test.go b/providers/dns/selfhostde/internal/client_test.go index 22949728c..8abda8fb6 100644 --- a/providers/dns/selfhostde/internal/client_test.go +++ b/providers/dns/selfhostde/internal/client_test.go @@ -1,41 +1,65 @@ 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 setupClient(server *httptest.Server) (*Client, error) { - client := NewClient("user", "secret") - client.baseURL = server.URL - client.HTTPClient = server.Client() +func setupTest(t *testing.T) (*Client, *http.ServeMux) { + t.Helper() - return client, nil + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := NewClient("user", "secret") + serverURL, err := url.Parse(server.URL) + require.NoError(t, err) + + client.baseURL = serverURL.String() + + return client, mux } 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) + client, mux := setupTest(t) - err := client.UpdateTXTRecord(t.Context(), "123456", "txt") + mux.HandleFunc("GET /", func(rw http.ResponseWriter, req *http.Request) { + query := req.URL.Query() + + fields := map[string]string{ + "username": "user", + "password": "secret", + "rid": "123456", + "content": "txt", + } + + for k, v := range fields { + value := query.Get(k) + if value != v { + http.Error(rw, fmt.Sprintf("%s: unexpected value: %s (%s)", k, value, v), http.StatusBadRequest) + return + } + } + }) + + err := client.UpdateTXTRecord(context.Background(), "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) + client, mux := setupTest(t) - err := client.UpdateTXTRecord(t.Context(), "123456", "txt") - require.EqualError(t, err, "unexpected status code: [status code: 400] body: ") + mux.HandleFunc("GET /", func(rw http.ResponseWriter, _ *http.Request) { + http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + }) + + err := client.UpdateTXTRecord(context.Background(), "123456", "txt") + require.Error(t, err) } diff --git a/providers/dns/selfhostde/mapping.go b/providers/dns/selfhostde/mapping.go index fe11ceda1..0984419ef 100644 --- a/providers/dns/selfhostde/mapping.go +++ b/providers/dns/selfhostde/mapping.go @@ -88,10 +88,8 @@ func parseLine(line string) (string, *Seq, error) { name, rawIDs := line[:idx], line[idx+1:] - var ( - ids []string - count int - ) + var ids []string + var count int for { idx, err = safeIndex(rawIDs, recordSep) diff --git a/providers/dns/selfhostde/selfhostde.go b/providers/dns/selfhostde/selfhostde.go index 035cd5363..0fea9f1d0 100644 --- a/providers/dns/selfhostde/selfhostde.go +++ b/providers/dns/selfhostde/selfhostde.go @@ -13,7 +13,6 @@ import ( "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" ) @@ -133,8 +132,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient } - client.HTTPClient = clientdebug.Wrap(client.HTTPClient) - return &DNSProvider{ config: config, client: client, @@ -176,7 +173,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("selfhostde: unknown record ID for %q", dns01.UnFqdn(info.EffectiveFQDN)) } @@ -186,9 +182,5 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { 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 index bd22c6c41..eba96fce2 100644 --- a/providers/dns/selfhostde/selfhostde.toml +++ b/providers/dns/selfhostde/selfhostde.toml @@ -8,7 +8,7 @@ Example = ''' SELFHOSTDE_USERNAME=xxx \ SELFHOSTDE_PASSWORD=yyy \ SELFHOSTDE_RECORDS_MAPPING=my.example.com:123 \ -lego --dns selfhostde -d '*.example.com' -d example.com run +lego --email you@example.com --dns selfhostde -d '*.example.com' -d example.com run ''' Additional = """ @@ -48,7 +48,7 @@ The resulting environment variable would then be: `SELFHOSTDE_RECORDS_MAPPING=my 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)" + SELFHOSTDE_POLLING_INTERVAL = "Time between DNS propagation check" + SELFHOSTDE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + SELFHOSTDE_TTL = "The TTL of the TXT record used for the DNS challenge" + SELFHOSTDE_HTTP_TIMEOUT = "API request timeout" diff --git a/providers/dns/selfhostde/selfhostde_test.go b/providers/dns/selfhostde/selfhostde_test.go index 7c12195fa..1161049b0 100644 --- a/providers/dns/selfhostde/selfhostde_test.go +++ b/providers/dns/selfhostde/selfhostde_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) @@ -186,7 +185,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -200,7 +198,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() 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..8597d7e12 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) } 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,16 +196,26 @@ 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) 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..c0c1662f6 100644 --- a/providers/dns/servercow/servercow.go +++ b/providers/dns/servercow/servercow.go @@ -12,7 +12,6 @@ import ( "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" ) @@ -45,7 +44,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 +85,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 +137,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 +191,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 } diff --git a/providers/dns/servercow/servercow.toml b/providers/dns/servercow/servercow.toml index 5cbacbb88..e9ec36be9 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 -d '*.example.com' -d example.com 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..dec1540c8 100644 --- a/providers/dns/shellrent/shellrent.go +++ b/providers/dns/shellrent/shellrent.go @@ -12,7 +12,6 @@ import ( "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" ) @@ -104,8 +103,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 +123,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 +161,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 +170,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..1e19e2d0d 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 -d '*.example.com' -d example.com 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..d2bfb1874 100644 --- a/providers/dns/simply/simply.go +++ b/providers/dns/simply/simply.go @@ -12,7 +12,6 @@ import ( "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" ) @@ -100,8 +99,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 +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("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..15cf7feb2 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 -d '*.example.com' -d example.com 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..80f5ea295 100644 --- a/providers/dns/sonic/sonic.go +++ b/providers/dns/sonic/sonic.go @@ -11,7 +11,6 @@ import ( "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" ) @@ -92,8 +91,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..f871d3f94 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 -d '*.example.com' -d example.com 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..8a1a2d09e 100644 --- a/providers/dns/stackpath/stackpath.go +++ b/providers/dns/stackpath/stackpath.go @@ -12,7 +12,6 @@ import ( "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" ) @@ -44,7 +43,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 +86,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..307922ee2 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 -d '*.example.com' -d example.com 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 index 965638b1d..a68008d34 100644 --- a/providers/dns/technitium/internal/client.go +++ b/providers/dns/technitium/internal/client.go @@ -125,7 +125,6 @@ func (c *Client) newFormRequest(ctx context.Context, endpoint *url.URL, payload 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) @@ -150,7 +149,6 @@ 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) diff --git a/providers/dns/technitium/internal/client_test.go b/providers/dns/technitium/internal/client_test.go index cd6914918..326c1e8eb 100644 --- a/providers/dns/technitium/internal/client_test.go +++ b/providers/dns/technitium/internal/client_test.go @@ -1,39 +1,51 @@ 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(server.URL, "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().WithContentTypeFromURLEncoded()) + mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { + 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 + } + }) + + client, err := NewClient(server.URL, "secret") + require.NoError(t, err) + + client.HTTPClient = server.Client() + + return client } 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) + client := setupTest(t, "POST /api/zones/records/add", "add-record.json") record := Record{ Domain: "_acme-challenge.example.com", @@ -41,7 +53,7 @@ func TestClient_AddRecord(t *testing.T) { Text: "txtTXTtxt", } - newRecord, err := client.AddRecord(t.Context(), record) + newRecord, err := client.AddRecord(context.Background(), record) require.NoError(t, err) expected := &Record{Name: "example.com", Type: "A"} @@ -50,10 +62,7 @@ func TestClient_AddRecord(t *testing.T) { } func TestClient_AddRecord_error(t *testing.T) { - client := mockBuilder(). - Route("POST /api/zones/records/add", - servermock.ResponseFromFixture("error.json")). - Build(t) + client := setupTest(t, "POST /api/zones/records/add", "error.json") record := Record{ Domain: "_acme-challenge.example.com", @@ -61,22 +70,14 @@ func TestClient_AddRecord_error(t *testing.T) { Text: "txtTXTtxt", } - _, err := client.AddRecord(t.Context(), record) + _, err := client.AddRecord(context.Background(), 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) + client := setupTest(t, "POST /api/zones/records/delete", "delete-record.json") record := Record{ Domain: "_acme-challenge.example.com", @@ -84,15 +85,12 @@ func TestClient_DeleteRecord(t *testing.T) { Text: "txtTXTtxt", } - err := client.DeleteRecord(t.Context(), record) + err := client.DeleteRecord(context.Background(), 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) + client := setupTest(t, "POST /api/zones/records/delete", "error.json") record := Record{ Domain: "_acme-challenge.example.com", @@ -100,7 +98,7 @@ func TestClient_DeleteRecord_error(t *testing.T) { Text: "txtTXTtxt", } - err := client.DeleteRecord(t.Context(), record) + err := client.DeleteRecord(context.Background(), 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/technitium.go b/providers/dns/technitium/technitium.go index fc60c09ad..b2cf2d701 100644 --- a/providers/dns/technitium/technitium.go +++ b/providers/dns/technitium/technitium.go @@ -11,7 +11,6 @@ import ( "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" ) @@ -88,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, diff --git a/providers/dns/technitium/technitium.toml b/providers/dns/technitium/technitium.toml index ac1fc6466..54502957f 100644 --- a/providers/dns/technitium/technitium.toml +++ b/providers/dns/technitium/technitium.toml @@ -7,7 +7,7 @@ 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 +lego --email you@example.com --dns technitium -d '*.example.com' -d example.com run ''' Additional = ''' @@ -23,10 +23,10 @@ Technitium recommends to use it in production over the HTTP API. 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)" + TECHNITIUM_POLLING_INTERVAL = "Time between DNS propagation check" + TECHNITIUM_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + TECHNITIUM_TTL = "The TTL of the TXT record used for the DNS challenge" + TECHNITIUM_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://github.com/TechnitiumSoftware/DnsServer/blob/0f83d23e605956b66ac76921199e241d9cc061bd/APIDOCS.md" diff --git a/providers/dns/technitium/technitium_test.go b/providers/dns/technitium/technitium_test.go index 4eee530fd..da50b6fe6 100644 --- a/providers/dns/technitium/technitium_test.go +++ b/providers/dns/technitium/technitium_test.go @@ -50,7 +50,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/tencentcloud/tencentcloud.go b/providers/dns/tencentcloud/tencentcloud.go index 00e41e93e..0b662f8c7 100644 --- a/providers/dns/tencentcloud/tencentcloud.go +++ b/providers/dns/tencentcloud/tencentcloud.go @@ -2,7 +2,6 @@ package tencentcloud import ( - "context" "errors" "fmt" "math" @@ -11,9 +10,9 @@ import ( "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. @@ -118,9 +117,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 +136,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 +148,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 +164,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..beb138e91 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 -d '*.example.com' -d example.com 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 index ec3c8703d..b3030861e 100644 --- a/providers/dns/timewebcloud/internal/client.go +++ b/providers/dns/timewebcloud/internal/client.go @@ -49,7 +49,6 @@ func (c *Client) CreateRecord(ctx context.Context, zone string, record DNSRecord } respData := &CreateRecordResponse{} - err = c.do(req, respData) if err != nil { return nil, err @@ -128,7 +127,6 @@ 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) diff --git a/providers/dns/timewebcloud/internal/client_test.go b/providers/dns/timewebcloud/internal/client_test.go index 9d16ba4c5..5bfa97fa0 100644 --- a/providers/dns/timewebcloud/internal/client_test.go +++ b/providers/dns/timewebcloud/internal/client_test.go @@ -1,35 +1,87 @@ 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) (*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 writeResponse(rw http.ResponseWriter, statusCode int, filename string) error { + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + return err + } + + defer func() { _ = file.Close() }() + + rw.WriteHeader(statusCode) + + _, err = io.Copy(rw, file) + if err != nil { + return err + } + + return nil } 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) + client, mux := setupTest(t) + + mux.HandleFunc("POST /v1/domains/example.com/dns-records", func(rw http.ResponseWriter, req *http.Request) { + 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.StatusInternalServerError) + return + } + + if string(bytes.TrimSpace(content)) != `{"type":"TXT","value":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","subdomain":"_acme-challenge"}` { + http.Error(rw, "invalid request body: "+string(content), http.StatusBadRequest) + return + } + + err = writeResponse(rw, http.StatusOK, "createDomainDNSRecord.json") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) payload := DNSRecord{ Type: "TXT", @@ -37,7 +89,7 @@ func TestClient_CreateRecord(t *testing.T) { SubDomain: "_acme-challenge", } - response, err := client.CreateRecord(t.Context(), "example.com.", payload) + response, err := client.CreateRecord(context.Background(), "example.com.", payload) require.NoError(t, err) expected := &DNSRecord{ @@ -49,37 +101,51 @@ func TestClient_CreateRecord(t *testing.T) { } 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) + client, mux := setupTest(t) - _, err := client.CreateRecord(t.Context(), "example.com.", DNSRecord{}) + mux.HandleFunc("POST /v1/domains/example.com/dns-records", func(rw http.ResponseWriter, _ *http.Request) { + err := writeResponse(rw, http.StatusBadRequest, "error_bad_request.json") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + _, err := client.CreateRecord(context.Background(), "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) + client, mux := setupTest(t) - err := client.DeleteRecord(t.Context(), "example.com.", 123) + mux.HandleFunc("DELETE /v1/domains/example.com/dns-records/123", func(rw http.ResponseWriter, req *http.Request) { + err := checkAuthorizationHeader(req) + if err != nil { + http.Error(rw, err.Error(), http.StatusUnauthorized) + return + } + + rw.WriteHeader(http.StatusNoContent) + }) + + err := client.DeleteRecord(context.Background(), "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) + client, mux := setupTest(t) - err := client.DeleteRecord(t.Context(), "example.com.", 123) + mux.HandleFunc("DELETE /v1/domains/example.com/dns-records/123", func(rw http.ResponseWriter, _ *http.Request) { + err := writeResponse(rw, http.StatusBadRequest, "error_unauthorized.json") + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + err := client.DeleteRecord(context.Background(), "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/types.go b/providers/dns/timewebcloud/internal/types.go index 80cdb2c70..81da4df5c 100644 --- a/providers/dns/timewebcloud/internal/types.go +++ b/providers/dns/timewebcloud/internal/types.go @@ -3,11 +3,9 @@ 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). + ID int `json:"id,omitempty"` + Type string `json:"type,omitempty"` + Value string `json:"value,omitempty"` SubDomain string `json:"subdomain,omitempty"` } diff --git a/providers/dns/timewebcloud/timewebcloud.go b/providers/dns/timewebcloud/timewebcloud.go index a599566e3..a2ab0dd65 100644 --- a/providers/dns/timewebcloud/timewebcloud.go +++ b/providers/dns/timewebcloud/timewebcloud.go @@ -12,7 +12,6 @@ import ( "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" ) @@ -82,11 +81,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("timewebcloud: authentication token is missing") } - client := internal.NewClient( - clientdebug.Wrap( - internal.OAuthStaticAccessToken(config.HTTPClient, config.AuthToken), - ), - ) + client := internal.NewClient(internal.OAuthStaticAccessToken(config.HTTPClient, config.AuthToken)) return &DNSProvider{ config: config, @@ -110,10 +105,15 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("timewebcloud: could not find zone for domain %q: %w", domain, err) } + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("timewebcloud: %w", err) + } + record := internal.DNSRecord{ Type: "TXT", Value: info.Value, - SubDomain: dns01.UnFqdn(info.EffectiveFQDN), + SubDomain: subDomain, } response, err := d.client.CreateRecord(context.Background(), authZone, record) @@ -140,7 +140,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("timewebcloud: unknown record ID for '%s'", info.EffectiveFQDN) } diff --git a/providers/dns/timewebcloud/timewebcloud.toml b/providers/dns/timewebcloud/timewebcloud.toml index c8bde636a..4f8d7e860 100644 --- a/providers/dns/timewebcloud/timewebcloud.toml +++ b/providers/dns/timewebcloud/timewebcloud.toml @@ -6,16 +6,16 @@ Since = "v4.20.0" Example = ''' TIMEWEBCLOUD_AUTH_TOKEN=xxxxxx \ -lego --dns timewebcloud -d '*.example.com' -d example.com run +lego --email you@example.com --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)" + TIMEWEBCLOUD_POLLING_INTERVAL = "Time between DNS propagation check" + TIMEWEBCLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + TIMEWEBCLOUD_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://timeweb.cloud/api-docs" diff --git a/providers/dns/timewebcloud/timewebcloud_test.go b/providers/dns/timewebcloud/timewebcloud_test.go index 26e107578..cd3e2e26f 100644 --- a/providers/dns/timewebcloud/timewebcloud_test.go +++ b/providers/dns/timewebcloud/timewebcloud_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) @@ -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/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/transip.go b/providers/dns/transip/transip.go index bc2913aa4..779704a21 100644 --- a/providers/dns/transip/transip.go +++ b/providers/dns/transip/transip.go @@ -4,7 +4,6 @@ package transip import ( "errors" "fmt" - "net/http" "time" "github.com/go-acme/lego/v4/challenge" @@ -24,7 +23,6 @@ const ( EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" - EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) var _ challenge.ProviderTimeout = (*DNSProvider)(nil) @@ -36,7 +34,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 +42,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 +73,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 +153,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..47059c551 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 -d '*.example.com' -d example.com 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..b42753680 100644 --- a/providers/dns/transip/transip_test.go +++ b/providers/dns/transip/transip_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) @@ -80,7 +79,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{ @@ -158,7 +156,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -172,7 +169,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..f95cf18e2 100644 --- a/providers/dns/ultradns/ultradns.go +++ b/providers/dns/ultradns/ultradns.go @@ -4,7 +4,6 @@ package ultradns import ( "errors" "fmt" - "net/http" "time" "github.com/go-acme/lego/v4/challenge" @@ -54,7 +53,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), } @@ -122,7 +121,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 +130,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..c6ff72eac 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 -d '*.example.com' -d example.com 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..eefa63ec3 100644 --- a/providers/dns/ultradns/ultradns_test.go +++ b/providers/dns/ultradns/ultradns_test.go @@ -177,7 +177,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..0f2c73c05 100644 --- a/providers/dns/variomedia/variomedia.go +++ b/providers/dns/variomedia/variomedia.go @@ -10,13 +10,11 @@ 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" ) @@ -93,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, @@ -165,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("variomedia: unknown record ID for '%s'", info.EffectiveFQDN) } @@ -180,30 +175,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..945a6f9f5 100644 --- a/providers/dns/variomedia/variomedia.toml +++ b/providers/dns/variomedia/variomedia.toml @@ -6,18 +6,18 @@ 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 -d '*.example.com' -d example.com run ''' [Configuration] [Configuration.Credentials] 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" + VARIOMEDIA_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..b56bce97b 100644 --- a/providers/dns/vegadns/vegadns.go +++ b/providers/dns/vegadns/vegadns.go @@ -2,17 +2,14 @@ package vegadns import ( - "context" "errors" "fmt" - "net/http" "time" + vegaClient "github.com/OpenDNS/vegadns2client" "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/vegadns" ) // Environment variables names. @@ -26,21 +23,18 @@ 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 +42,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 +75,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 +90,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..bf3a0f532 100644 --- a/providers/dns/vercel/vercel.go +++ b/providers/dns/vercel/vercel.go @@ -12,7 +12,6 @@ import ( "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" ) @@ -45,7 +44,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 +86,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 +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("vercel: unknown record ID for '%s'", info.EffectiveFQDN) } diff --git a/providers/dns/vercel/vercel.toml b/providers/dns/vercel/vercel.toml index 4700d6d78..60df41798 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 -d '*.example.com' -d example.com 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..08a2d4639 100644 --- a/providers/dns/versio/versio.go +++ b/providers/dns/versio/versio.go @@ -13,7 +13,6 @@ import ( "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" ) @@ -56,7 +55,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 +91,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 +108,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 +155,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("versio: %w", err) } - return nil } @@ -188,7 +182,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 +192,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..7fc27ebcd 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 -d '*.example.com' -d example.com 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..a206602da 100644 --- a/providers/dns/vinyldns/vinyldns.go +++ b/providers/dns/vinyldns/vinyldns.go @@ -2,17 +2,13 @@ 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 +17,25 @@ 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 +44,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 +66,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) } @@ -104,22 +91,13 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { UserAgent: useragent.Get(), }) - 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 +105,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 +117,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 +125,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 +135,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 +146,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 +162,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 +175,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..bdd07bae8 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 -d '*.example.com' -d example.com 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..e76e87137 100644 --- a/providers/dns/vkcloud/vkcloud.go +++ b/providers/dns/vkcloud/vkcloud.go @@ -119,7 +119,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 +129,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 +150,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 +159,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 +169,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 +191,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 +201,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 +218,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 +234,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..8e67e2670 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 -d '*.example.com' -d example.com 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 index 765d38adb..ed5544592 100644 --- a/providers/dns/volcengine/volcengine.go +++ b/providers/dns/volcengine/volcengine.go @@ -13,6 +13,7 @@ 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/ptr" + "github.com/miekg/dns" "github.com/volcengine/volc-sdk-golang/base" volc "github.com/volcengine/volc-sdk-golang/service/dns" ) @@ -62,7 +63,7 @@ func NewDefaultConfig() *Config { Region: env.GetOrDefaultString(EnvRegion, volc.DefaultRegion), TTL: env.GetOrDefaultInt(EnvTTL, defaultTTL), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 4*time.Minute), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 240*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, volc.Timeout*time.Second), } @@ -159,7 +160,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("volcengine: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token) } @@ -171,15 +171,13 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { 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) { + for _, index := range dns.Split(fqdn) { + domain := fqdn[index:] + lzr := &volc.ListZonesRequest{ Key: ptr.Pointer(dns01.UnFqdn(domain)), SearchMode: ptr.Pointer("exact"), diff --git a/providers/dns/volcengine/volcengine.toml b/providers/dns/volcengine/volcengine.toml index ceedcb18a..85431714f 100644 --- a/providers/dns/volcengine/volcengine.toml +++ b/providers/dns/volcengine/volcengine.toml @@ -7,7 +7,7 @@ Since = "v4.19.0" Example = ''' VOLC_ACCESSKEY=xxx \ VOLC_SECRETKEY=yyy \ -lego --dns volcengine -d '*.example.com' -d example.com run +lego --email you@example.com --dns volcengine -d '*.example.com' -d example.com run ''' [Configuration] @@ -18,10 +18,10 @@ lego --dns volcengine -d '*.example.com' -d example.com run 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)" + VOLC_POLLING_INTERVAL = "Time between DNS propagation check" + VOLC_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + VOLC_TTL = "The TTL of the TXT record used for the DNS challenge" + VOLC_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.volcengine.com/docs/6758/155086" diff --git a/providers/dns/volcengine/volcengine_test.go b/providers/dns/volcengine/volcengine_test.go index 0f79ed83a..5e9167612 100644 --- a/providers/dns/volcengine/volcengine_test.go +++ b/providers/dns/volcengine/volcengine_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) @@ -126,7 +125,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -140,7 +138,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/vscale/vscale.go b/providers/dns/vscale/vscale.go index a159db307..a500837bc 100644 --- a/providers/dns/vscale/vscale.go +++ b/providers/dns/vscale/vscale.go @@ -4,9 +4,11 @@ package vscale import ( + "context" "errors" "fmt" "net/http" + "net/url" "time" "github.com/go-acme/lego/v4/challenge" @@ -28,20 +30,27 @@ const ( EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) -const defaultBaseURL = "https://api.vscale.io/v1/domains" +const minTTL = 60 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 +59,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 +83,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..83aa6a513 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 -d '*.example.com' -d example.com 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..7672d2054 100644 --- a/providers/dns/vultr/vultr.go +++ b/providers/dns/vultr/vultr.go @@ -13,7 +13,6 @@ import ( "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" "golang.org/x/oauth2" ) @@ -39,7 +38,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 +84,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,7 +106,7 @@ 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 + `"`, @@ -136,7 +135,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 { @@ -206,7 +204,6 @@ 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) if err != nil { diff --git a/providers/dns/vultr/vultr.toml b/providers/dns/vultr/vultr.toml index 78e878bea..83b896f77 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 -d '*.example.com' -d example.com 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..71d8ad414 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,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" "github.com/vultr/govultr/v3" @@ -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..78905e22c 100644 --- a/providers/dns/webnames/webnames.go +++ b/providers/dns/webnames/webnames.go @@ -6,20 +6,17 @@ 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" @@ -42,10 +39,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 +54,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 +70,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 +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 } @@ -97,17 +92,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 +115,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 +137,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..030d385c9 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 -d '*.example.com' -d example.com 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 53% rename from providers/dns/internal/active24/internal/client.go rename to providers/dns/websupport/internal/client.go index 69e94b367..4fef0be91 100644 --- a/providers/dns/internal/active24/internal/client.go +++ b/providers/dns/websupport/internal/client.go @@ -12,118 +12,147 @@ 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()) + err := c.sign(req, time.Now().UTC()) if err != nil { - return fmt.Errorf("sign request: %w", err) + return fmt.Errorf("signature: %w", err) } resp, err := c.HTTPClient.Do(req) @@ -133,14 +162,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 +179,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 +230,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 +237,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..db31315d8 100644 --- a/providers/dns/websupport/websupport.go +++ b/providers/dns/websupport/websupport.go @@ -2,19 +2,19 @@ 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 +26,30 @@ const ( 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 = 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 +58,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 +86,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..d1a0af7dc 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 you@example.com --dns websupport -d '*.example.com' -d example.com 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..85187ec46 100644 --- a/providers/dns/wedos/wedos.go +++ b/providers/dns/wedos/wedos.go @@ -12,7 +12,6 @@ import ( "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" ) @@ -95,8 +94,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..64845536e 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 -d '*.example.com' -d example.com 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/internal/westcn/internal/client.go b/providers/dns/westcn/internal/client.go similarity index 96% rename from providers/dns/internal/westcn/internal/client.go rename to providers/dns/westcn/internal/client.go index 621c7865f..4d967f5e1 100644 --- a/providers/dns/internal/westcn/internal/client.go +++ b/providers/dns/westcn/internal/client.go @@ -14,8 +14,8 @@ import ( "strings" "time" - "github.com/go-acme/lego/v4/providers/dns/internal/errutils" querystring "github.com/google/go-querystring/query" + "github.com/nrdcg/mailinabox/errutils" "golang.org/x/text/encoding" "golang.org/x/text/encoding/simplifiedchinese" "golang.org/x/text/transform" @@ -30,7 +30,7 @@ type Client struct { encoder *encoding.Encoder - BaseURL *url.URL + baseURL *url.URL HTTPClient *http.Client } @@ -46,7 +46,7 @@ func NewClient(username, password string) (*Client, error) { username: username, password: password, encoder: simplifiedchinese.GBK.NewEncoder(), - BaseURL: baseURL, + baseURL: baseURL, HTTPClient: &http.Client{Timeout: 10 * time.Second}, }, nil } @@ -116,7 +116,7 @@ func (c *Client) newRequest(ctx context.Context, p, act string, form url.Values) return nil, err } - endpoint := c.BaseURL.JoinPath(p, "/") + endpoint := c.baseURL.JoinPath(p, "/") query := endpoint.Query() query.Set("act", act) diff --git a/providers/dns/westcn/internal/client_test.go b/providers/dns/westcn/internal/client_test.go new file mode 100644 index 000000000..ed0c7dc1a --- /dev/null +++ b/providers/dns/westcn/internal/client_test.go @@ -0,0 +1,215 @@ +package internal + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/text/encoding/simplifiedchinese" +) + +type formExpectation func(values url.Values) error + +func setupTest(t *testing.T, filename string, expectations ...formExpectation) *Client { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc("POST /", func(rw http.ResponseWriter, req *http.Request) { + err := req.ParseForm() + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + commons := []formExpectation{ + expectValue("username", "user"), + expectNotEmpty("time"), + expectNotEmpty("token"), + } + + for _, common := range commons { + err = common(req.Form) + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + } + + for _, expectation := range expectations { + err = expectation(req.Form) + if err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + } + + rw.Header().Set("Content-Type", "application/json; Charset=gb2312") + + 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(http.StatusOK) + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + client, err := NewClient("user", "secret") + require.NoError(t, err) + + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client +} + +func expectValue(key, value string) formExpectation { + return func(values url.Values) error { + if values.Get(key) != value { + return fmt.Errorf("expected %s, got %s", value, values.Get(key)) + } + + return nil + } +} + +func expectNotEmpty(key string) formExpectation { + return func(values url.Values) error { + if values.Get(key) == "" { + return fmt.Errorf("%s missing", key) + } + + return nil + } +} + +func noop() formExpectation { + return func(_ url.Values) error { + return nil + } +} + +func TestClientAddRecord(t *testing.T) { + expectValue("act", "adddnsrecord") + + client := setupTest(t, "adddnsrecord.json", + expectValue("act", "adddnsrecord"), + expectValue("domain", "example.com"), + expectValue("host", "@"), + expectValue("type", "TXT"), + expectValue("value", "txtTXTtxt"), + expectValue("ttl", "60"), + ) + + record := Record{ + Domain: "example.com", + Host: "@", + Type: "TXT", + Value: "txtTXTtxt", + TTL: 60, + } + + id, err := client.AddRecord(context.Background(), record) + require.NoError(t, err) + + assert.Equal(t, 123456, id) +} + +func TestClientAddRecord_error(t *testing.T) { + client := setupTest(t, "error.json", noop()) + + record := Record{ + Domain: "example.com", + Host: "@", + Type: "TXT", + Value: "txtTXTtxt", + TTL: 60, + } + + _, err := client.AddRecord(context.Background(), record) + require.Error(t, err) + + require.EqualError(t, err, "10000: username,time,token必传 (500)") +} + +func TestClientDeleteRecord(t *testing.T) { + client := setupTest(t, "deldnsrecord.json", + expectValue("act", "deldnsrecord"), + expectValue("domain", "example.com"), + ) + + err := client.DeleteRecord(context.Background(), "example.com", 123) + require.NoError(t, err) +} + +func TestClientDeleteRecord_error(t *testing.T) { + client := setupTest(t, "error.json", noop()) + + err := client.DeleteRecord(context.Background(), "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/westcn/internal/fixtures/adddnsrecord.json similarity index 100% rename from providers/dns/internal/westcn/internal/fixtures/adddnsrecord.json rename to providers/dns/westcn/internal/fixtures/adddnsrecord.json diff --git a/providers/dns/internal/westcn/internal/fixtures/deldnsrecord.json b/providers/dns/westcn/internal/fixtures/deldnsrecord.json similarity index 100% rename from providers/dns/internal/westcn/internal/fixtures/deldnsrecord.json rename to providers/dns/westcn/internal/fixtures/deldnsrecord.json diff --git a/providers/dns/internal/westcn/internal/fixtures/error.json b/providers/dns/westcn/internal/fixtures/error.json similarity index 100% rename from providers/dns/internal/westcn/internal/fixtures/error.json rename to providers/dns/westcn/internal/fixtures/error.json diff --git a/providers/dns/internal/westcn/internal/types.go b/providers/dns/westcn/internal/types.go similarity index 100% rename from providers/dns/internal/westcn/internal/types.go rename to providers/dns/westcn/internal/types.go diff --git a/providers/dns/westcn/westcn.go b/providers/dns/westcn/westcn.go index 1906f9737..37f357b70 100644 --- a/providers/dns/westcn/westcn.go +++ b/providers/dns/westcn/westcn.go @@ -2,14 +2,17 @@ package westcn 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/westcn" + "github.com/go-acme/lego/v4/providers/dns/westcn/internal" ) // Environment variables names. @@ -25,12 +28,18 @@ const ( 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 +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 { @@ -46,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 West.cn/西部数码. @@ -69,36 +82,88 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("westcn: the configuration of the DNS provider is nil") } - provider, err := westcn.NewDNSProviderConfig(config, defaultBaseURL) + client, err := internal.NewClient(config.Username, config.Password) if err != nil { return nil, fmt.Errorf("westcn: %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("westcn: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) if err != nil { return fmt.Errorf("westcn: %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("westcn: 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 { - err := d.prv.CleanUp(domain, token, keyAuth) + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { - return fmt.Errorf("westcn: %w", err) + return fmt.Errorf("westcn: 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("westcn: 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("westcn: 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.prv.Timeout() + return d.config.PropagationTimeout, d.config.PollingInterval } diff --git a/providers/dns/westcn/westcn.toml b/providers/dns/westcn/westcn.toml index 1b0cb0a7a..3b3914eac 100644 --- a/providers/dns/westcn/westcn.toml +++ b/providers/dns/westcn/westcn.toml @@ -7,7 +7,7 @@ Since = "v4.21.0" Example = ''' WESTCN_USERNAME="xxx" \ WESTCN_PASSWORD="yyy" \ -lego --dns westcn -d '*.example.com' -d example.com run +lego --email you@example.com --dns westcn -d '*.example.com' -d example.com run ''' [Configuration] @@ -15,10 +15,10 @@ lego --dns westcn -d '*.example.com' -d example.com run 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)" + WESTCN_POLLING_INTERVAL = "Time between DNS propagation check" + WESTCN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + WESTCN_TTL = "The TTL of the TXT record used for the DNS challenge" + WESTCN_HTTP_TIMEOUT = "API request timeout" [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 index a546d518e..71632d99f 100644 --- a/providers/dns/westcn/westcn_test.go +++ b/providers/dns/westcn/westcn_test.go @@ -50,7 +50,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 +59,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) } @@ -107,7 +107,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) } @@ -121,7 +122,6 @@ func TestLivePresent(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() require.NoError(t, err) @@ -135,7 +135,6 @@ func TestLiveCleanUp(t *testing.T) { } envTest.RestoreEnv() - provider, err := NewDNSProvider() 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..c51602f67 100644 --- a/providers/dns/yandex/yandex.go +++ b/providers/dns/yandex/yandex.go @@ -11,7 +11,6 @@ import ( "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" ) @@ -89,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 } @@ -136,7 +133,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 +153,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..91adf4658 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 -d '*.example.com' -d example.com 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..e2ee7beb2 100644 --- a/providers/dns/yandex360/yandex360.go +++ b/providers/dns/yandex360/yandex360.go @@ -13,9 +13,7 @@ import ( "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. @@ -99,8 +97,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 +108,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 +143,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..88e4036ab 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 -d '*.example.com' -d example.com 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..22da14404 100644 --- a/providers/dns/yandexcloud/yandexcloud.go +++ b/providers/dns/yandexcloud/yandexcloud.go @@ -14,12 +14,9 @@ import ( "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. @@ -57,7 +54,7 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - client ycdns.DnsZoneClient + client *ycsdk.SDK config *Config } @@ -94,19 +91,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 +113,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 +135,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 +144,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 +154,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 +176,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 +186,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 +204,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 +237,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 +265,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 +285,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..c19b9c1cc 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 -d '*.example.com' -d example.com 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 -d '*.example.com' -d example.com 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..7dbbc4314 100644 --- a/providers/dns/zoneee/zoneee.go +++ b/providers/dns/zoneee/zoneee.go @@ -12,7 +12,6 @@ import ( "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" ) @@ -70,7 +69,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 +105,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 } @@ -143,7 +138,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { if err != nil { return fmt.Errorf("zoneee: %w", err) } - return nil } @@ -166,7 +160,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..5d95095e8 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 -d '*.example.com' -d example.com 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..8c7a2943f 100644 --- a/providers/dns/zonomi/zonomi.go +++ b/providers/dns/zonomi/zonomi.go @@ -2,6 +2,7 @@ package zonomi import ( + "context" "errors" "fmt" "net/http" @@ -25,17 +26,22 @@ 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 +52,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 +76,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 +125,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..9780323a7 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 -d '*.example.com' -d example.com 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/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index 9c4bc9e61..053c3c4e7 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -6,28 +6,17 @@ 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 +26,15 @@ 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,33 +44,23 @@ 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" @@ -101,15 +75,9 @@ 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" @@ -119,29 +87,22 @@ import ( "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" @@ -166,35 +127,28 @@ import ( "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 +156,28 @@ import ( // NewDNSChallengeProviderByName Factory for DNS providers. func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { switch name { + case "manual": + return dns01.NewDNSProviderManual() case "acme-dns", "acmedns": 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 "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 +196,14 @@ 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": @@ -290,8 +214,6 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return digitalocean.NewDNSProvider() case "directadmin": return directadmin.NewDNSProvider() - case "dnsexit": - return dnsexit.NewDNSProvider() case "dnshomede": return dnshomede.NewDNSProvider() case "dnsimple": @@ -310,32 +232,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": 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,24 +256,16 @@ 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": @@ -392,24 +294,12 @@ 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": @@ -428,18 +318,12 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return mailinabox.NewDNSProvider() case "manageengine": return manageengine.NewDNSProvider() - case "manual": - return manual.NewDNSProvider() 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 +334,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 +350,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": @@ -522,26 +398,18 @@ 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,8 +420,6 @@ 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": @@ -562,10 +428,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { 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": @@ -578,8 +442,6 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return yandex360.NewDNSProvider() case "yandexcloud": return yandexcloud.NewDNSProvider() - case "zoneedit": - return zoneedit.NewDNSProvider() case "zoneee": return zoneee.NewDNSProvider() case "zonomi": 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)